modforge-cli 0.2.3__tar.gz → 0.2.4__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.
Files changed (26) hide show
  1. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/PKG-INFO +54 -1
  2. modforge_cli-0.2.4/README.md +90 -0
  3. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/pyproject.toml +1 -1
  4. modforge_cli-0.2.4/src/modforge_cli/__init__.py +3 -0
  5. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/__main__.py +0 -5
  6. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/__version__.py +1 -1
  7. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/export.py +71 -50
  8. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/downloader.py +48 -3
  9. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/resolver.py +6 -2
  10. modforge_cli-0.2.4/src/modforge_cli/test.py +18 -0
  11. modforge_cli-0.2.3/README.md +0 -37
  12. modforge_cli-0.2.3/src/modforge_cli/__init__.py +0 -7
  13. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/LICENSE +0 -0
  14. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/api/__init__.py +0 -0
  15. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/api/modrinth.py +0 -0
  16. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/__init__.py +0 -0
  17. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/modpack.py +0 -0
  18. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/project.py +0 -0
  19. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/setup.py +0 -0
  20. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/shared.py +0 -0
  21. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/sklauncher.py +0 -0
  22. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/utils.py +0 -0
  23. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/__init__.py +0 -0
  24. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/models.py +0 -0
  25. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/policy.py +0 -0
  26. {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modforge-cli
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: ModForge-CLI — a Modrinth-based Minecraft modpack builder
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -68,3 +68,56 @@ poetry install
68
68
  pip install -r requirements.txt
69
69
  ```
70
70
 
71
+ ## Example
72
+
73
+ ```bash
74
+ modforge-cli setup --loader-version 0.18.4 TestPack
75
+ cd TestPack
76
+ modforge-cli add sodium
77
+ modforge-cli add "Fabric API"
78
+ modforge-cli add "Cloth Config"
79
+ modforge-cli add "ferriteCore"
80
+ modforge-cli add "Entity Culling"
81
+ modforge-cli add "Mod Menu"
82
+ modforge-cli add "Lithium"
83
+ modforge-cli add "ImmediatelyFast"
84
+ modforge-cli add "yacl"
85
+ modforge-cli add "Xaero's minimap"
86
+ modforge-cli add "Fabric Language Kotlin"
87
+ modforge-cli add "JEI"
88
+ modforge-cli add "3D Skin Layers"
89
+ modforge-cli add "More Culling"
90
+ modforge-cli add "Zoomify"
91
+ modforge-cli add "Mouse Tweaks"
92
+ modforge-cli add "Sound Physics Remastered"
93
+ modforge-cli add "LambDynamicLights"
94
+ modforge-cli add "Krypton"
95
+ modforge-cli add "AmbientSounds"
96
+ modforge-cli add "BadOptimizations"
97
+ modforge-cli add "Debugify"
98
+ modforge-cli add "Veinminer Enchantment"
99
+ modforge-cli add "Packet Fixer"
100
+ modforge-cli add "CustomSkinLoader"
101
+ modforge-cli add "Cubes Without Borders"
102
+ modforge-cli add "Particle Rain"
103
+ modforge-cli add "Chunky"
104
+ modforge-cli add "Fusion (Connected Textures)"
105
+ modforge-cli add "Do a Barrel Roll"
106
+ modforge-cli add "Resourcify"
107
+ modforge-cli add "Particle Core"
108
+ modforge-cli add "Drip Sounds"
109
+ modforge-cli add "ScalableLux"
110
+ modforge-cli add "Cull Leaves"
111
+ modforge-cli add "rrls"
112
+ modforge-cli add "ModernFix-mVUS"
113
+ modforge-cli add "NoisiumForked"
114
+ modforge-cli add "KryptonFNP Patcher"
115
+ modforge-cli add "Podium"
116
+ modforge-cli add "Iris"
117
+ modforge-cli add "first-person-model"
118
+ modforge-cli add "Helium"
119
+ modforge-cli resolve
120
+ modforge-cli build
121
+ modforge-cli export
122
+ ```
123
+
@@ -0,0 +1,90 @@
1
+ # ModForge-CLI ⛏
2
+
3
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue)](https://www.python.org/)
4
+ [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
+ [![Modrinth API v2](https://img.shields.io/badge/Modrinth-API%20v2-orange)](https://docs.modrinth.com/api-spec)
6
+
7
+ **ModForge-CLI** is a powerful CLI tool for building and managing custom Minecraft modpacks using the Modrinth API v2.
8
+
9
+ Search for projects, fetch versions, validate manifests, download mods with hash checks, and generate complete files — all from the terminal.
10
+
11
+ Ideal for modpack developers, server admins, and automation scripts.
12
+
13
+ ## Terminal Banner
14
+
15
+ When you run ModForge-CLI, you'll be greeted with this colorful Minecraft-themed banner
16
+
17
+ ## Key Features
18
+
19
+ - **Modrinth API v2 Integration**: Search projects, list versions, fetch metadata in bulk.
20
+ - **Modpack Management**: Read/validate `modrinth.index.json`, build packs from metadata.
21
+ - **Validation**: Full JSON Schema checks + optional Pydantic models for strict typing.
22
+
23
+ ## Installation
24
+
25
+ Requires **Python 3.13+**.
26
+
27
+ **Recommended (Poetry)**:
28
+
29
+ ```bash
30
+ poetry install
31
+ ```
32
+
33
+ **Alternative (pip)**:
34
+
35
+ ```bash
36
+ pip install -r requirements.txt
37
+ ```
38
+
39
+ ## Example
40
+
41
+ ```bash
42
+ modforge-cli setup --loader-version 0.18.4 TestPack
43
+ cd TestPack
44
+ modforge-cli add sodium
45
+ modforge-cli add "Fabric API"
46
+ modforge-cli add "Cloth Config"
47
+ modforge-cli add "ferriteCore"
48
+ modforge-cli add "Entity Culling"
49
+ modforge-cli add "Mod Menu"
50
+ modforge-cli add "Lithium"
51
+ modforge-cli add "ImmediatelyFast"
52
+ modforge-cli add "yacl"
53
+ modforge-cli add "Xaero's minimap"
54
+ modforge-cli add "Fabric Language Kotlin"
55
+ modforge-cli add "JEI"
56
+ modforge-cli add "3D Skin Layers"
57
+ modforge-cli add "More Culling"
58
+ modforge-cli add "Zoomify"
59
+ modforge-cli add "Mouse Tweaks"
60
+ modforge-cli add "Sound Physics Remastered"
61
+ modforge-cli add "LambDynamicLights"
62
+ modforge-cli add "Krypton"
63
+ modforge-cli add "AmbientSounds"
64
+ modforge-cli add "BadOptimizations"
65
+ modforge-cli add "Debugify"
66
+ modforge-cli add "Veinminer Enchantment"
67
+ modforge-cli add "Packet Fixer"
68
+ modforge-cli add "CustomSkinLoader"
69
+ modforge-cli add "Cubes Without Borders"
70
+ modforge-cli add "Particle Rain"
71
+ modforge-cli add "Chunky"
72
+ modforge-cli add "Fusion (Connected Textures)"
73
+ modforge-cli add "Do a Barrel Roll"
74
+ modforge-cli add "Resourcify"
75
+ modforge-cli add "Particle Core"
76
+ modforge-cli add "Drip Sounds"
77
+ modforge-cli add "ScalableLux"
78
+ modforge-cli add "Cull Leaves"
79
+ modforge-cli add "rrls"
80
+ modforge-cli add "ModernFix-mVUS"
81
+ modforge-cli add "NoisiumForked"
82
+ modforge-cli add "KryptonFNP Patcher"
83
+ modforge-cli add "Podium"
84
+ modforge-cli add "Iris"
85
+ modforge-cli add "first-person-model"
86
+ modforge-cli add "Helium"
87
+ modforge-cli resolve
88
+ modforge-cli build
89
+ modforge-cli export
90
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "modforge-cli"
3
- version = "0.2.3"
3
+ version = "0.2.4"
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" }
@@ -0,0 +1,3 @@
1
+ """
2
+ CLI package for ModForge-CLI
3
+ """
@@ -102,9 +102,6 @@ def main_callback(
102
102
  console.print(" [green]validate[/green] Check .mrpack for issues")
103
103
  console.print(" [green]sklauncher[/green] Create SKLauncher profile (no .mrpack)")
104
104
  console.print(" [green]remove[/green] Remove a modpack project")
105
- console.print("\n[bold cyan]Utility:[/bold cyan]")
106
- console.print(" [green]self-update[/green] Update ModForge-CLI")
107
- console.print(" [green]doctor[/green] Validate installation")
108
105
  console.print("\nRun [white]ModForge-CLI --help[/white] for details.\n")
109
106
 
110
107
 
@@ -119,8 +116,6 @@ app.command()(modpack.build)
119
116
  app.command()(export.export)
120
117
  app.command()(export.validate)
121
118
  app.command()(sklauncher.sklauncher)
122
- app.command()(utils.doctor)
123
- app.command("self-update")(utils.self_update_cmd)
124
119
 
125
120
 
126
121
  def main() -> None:
@@ -1,5 +1,5 @@
1
1
  """
2
2
  Auto-generated file. DO NOT EDIT.
3
3
  """
4
- __version__ = "0.2.3"
4
+ __version__ = "0.2.4"
5
5
  __author__ = "Frank1o3"
@@ -4,8 +4,6 @@ Export and validation commands
4
4
 
5
5
  import json
6
6
  from pathlib import Path
7
- import shutil
8
- import tempfile
9
7
  import zipfile
10
8
  from zipfile import ZIP_DEFLATED, ZipFile
11
9
 
@@ -21,6 +19,7 @@ app = typer.Typer()
21
19
  def export(pack_name: str | None = None) -> None:
22
20
  """Create final .mrpack file"""
23
21
 
22
+ # Resolve pack name
24
23
  if not pack_name:
25
24
  manifest = get_manifest(console, Path.cwd())
26
25
  if manifest:
@@ -41,65 +40,54 @@ def export(pack_name: str | None = None) -> None:
41
40
 
42
41
  console.print("[cyan]Exporting modpack...[/cyan]")
43
42
 
44
- mods_dir = pack_path / "mods"
45
43
  index_file = pack_path / "modrinth.index.json"
46
44
 
47
- if not mods_dir.exists() or not any(mods_dir.iterdir()):
48
- console.print("[red]No mods found. Run 'ModForge-CLI build' first[/red]")
49
- raise typer.Exit(1)
50
-
51
45
  if not index_file.exists():
52
46
  console.print("[red]No modrinth.index.json found[/red]")
53
47
  raise typer.Exit(1)
54
48
 
55
- # Validate index has files
56
- index_data = json.loads(index_file.read_text())
49
+ # Validate index JSON before export
50
+ try:
51
+ index_data = json.loads(index_file.read_text())
52
+ except json.JSONDecodeError as e:
53
+ console.print("[red]Invalid modrinth.index.json[/red]")
54
+ console.print(f"[dim]{e}[/dim]")
55
+ raise typer.Exit(1) from e
56
+
57
57
  if not index_data.get("files"):
58
58
  console.print("[yellow]Warning: No files registered in index[/yellow]")
59
- console.print("[yellow]This might cause issues. Run 'ModForge-CLI build' again.[/yellow]")
60
-
61
- # Create .mrpack
62
- with tempfile.TemporaryDirectory() as tmpdir:
63
- tmp_path = Path(tmpdir)
64
-
65
- # Copy modrinth.index.json to root
66
- shutil.copy2(index_file, tmp_path / "modrinth.index.json")
59
+ console.print("[yellow]Run 'ModForge-CLI build' if this is unintended.[/yellow]")
67
60
 
68
- # Copy overrides if they exist
69
- overrides_src = pack_path / "overrides"
70
- if overrides_src.exists():
71
- overrides_dst = tmp_path / "overrides"
72
- shutil.copytree(overrides_src, overrides_dst)
73
- console.print("[green]✓ Copied overrides[/green]")
61
+ # Output file (.mrpack extension)
62
+ mrpack_path = pack_path.parent / f"{pack_name}.mrpack"
74
63
 
75
- # Create .mrpack
76
- mrpack_path = pack_path.parent / f"{pack_name}.mrpack"
64
+ # Create archive
65
+ with ZipFile(mrpack_path, "w", ZIP_DEFLATED) as zipf:
66
+ for file_path in pack_path.rglob("*"):
67
+ if file_path.is_file():
68
+ arcname = file_path.relative_to(pack_path)
69
+ zipf.write(file_path, arcname)
77
70
 
78
- with ZipFile(mrpack_path, "w", ZIP_DEFLATED) as zipf:
79
- # Add modrinth.index.json at root
80
- zipf.write(tmp_path / "modrinth.index.json", "modrinth.index.json")
71
+ console.print(f"[green bold]✓ Exported to {mrpack_path}[/green bold]")
81
72
 
82
- # Add overrides folder if exists
83
- if overrides_src.exists():
84
- for file_path in (tmp_path / "overrides").rglob("*"):
85
- if file_path.is_file():
86
- arcname = str(file_path.relative_to(tmp_path))
87
- zipf.write(file_path, arcname)
73
+ # ---- Summary ----
74
+ file_count = len(index_data.get("files", []))
88
75
 
89
- console.print(f"[green bold]✓ Exported to {mrpack_path}[/green bold]")
76
+ console.print("\n[cyan]Summary:[/cyan]")
77
+ console.print(f" Files registered in index: {file_count}")
78
+ console.print(f" Minecraft: {index_data['dependencies'].get('minecraft')}")
90
79
 
91
- # Show summary
92
- file_count = len(index_data.get("files", []))
93
- console.print("\n[cyan]Summary:[/cyan]")
94
- console.print(f" Files registered: {file_count}")
95
- console.print(f" Minecraft: {index_data['dependencies'].get('minecraft')}")
80
+ for loader in ["fabric-loader", "quilt-loader", "forge", "neoforge"]:
81
+ if loader in index_data["dependencies"]:
82
+ console.print(f" Loader: {loader} {index_data['dependencies'][loader]}")
96
83
 
97
- # Show loader
98
- for loader in ["fabric-loader", "quilt-loader", "forge", "neoforge"]:
99
- if loader in index_data["dependencies"]:
100
- console.print(f" Loader: {loader} {index_data['dependencies'][loader]}")
84
+ has_env = any("env" in f for f in index_data.get("files", []))
85
+ if has_env:
86
+ console.print(" [green]✓ Environment data included[/green]")
87
+ else:
88
+ console.print(" [yellow]⚠ No environment data (older format)[/yellow]")
101
89
 
102
- console.print("\n[dim]Import this in SKLauncher, Prism, ATLauncher, etc.[/dim]")
90
+ console.print("\n[dim]Import this in SKLauncher, Prism, ATLauncher, etc.[/dim]")
103
91
 
104
92
 
105
93
  @app.command()
@@ -153,6 +141,13 @@ def validate(mrpack_file: str | None = None) -> None:
153
141
  else:
154
142
  console.print(f"[green]✅ {field}: {value}[/green]")
155
143
 
144
+ # Check optional summary
145
+ if "summary" not in index_data:
146
+ warnings.append("Missing optional summary field")
147
+ console.print("[yellow]⚠️ Missing summary (optional but recommended)[/yellow]")
148
+ else:
149
+ console.print(f"[green]✅ summary: {index_data['summary'][:50]}...[/green]")
150
+
156
151
  # Check dependencies
157
152
  deps = index_data.get("dependencies", {})
158
153
  if "minecraft" not in deps:
@@ -192,6 +187,13 @@ def validate(mrpack_file: str | None = None) -> None:
192
187
  else:
193
188
  console.print("[green]✅ File structure looks good[/green]")
194
189
 
190
+ # Check path security
191
+ for file_entry in files_list:
192
+ path = file_entry.get("path", "")
193
+ if ".." in path or path.startswith(("/", "\\")):
194
+ issues.append(f"Security: invalid path: {path}")
195
+ console.print(f"[red]❌ SECURITY: Invalid path: {path}[/red]")
196
+
195
197
  # Check hashes
196
198
  if "hashes" in sample:
197
199
  if "sha1" not in sample["hashes"]:
@@ -206,12 +208,31 @@ def validate(mrpack_file: str | None = None) -> None:
206
208
  else:
207
209
  console.print("[green]✅ sha512 hashes present[/green]")
208
210
 
209
- # Check env field
211
+ # Check env field (NEW)
210
212
  if "env" not in sample:
211
213
  warnings.append("Files missing env field")
212
214
  console.print("[yellow]⚠️ Missing env field (recommended)[/yellow]")
213
215
  else:
214
- console.print("[green]✅ env field present[/green]")
216
+ env = sample["env"]
217
+ if "client" in env and "server" in env:
218
+ console.print("[green]✅ env field present[/green]")
219
+ else:
220
+ warnings.append("env field incomplete")
221
+ console.print("[yellow]⚠️ env field incomplete[/yellow]")
222
+
223
+ # Check for overrides and server-overrides
224
+ has_overrides = any(f.startswith("overrides/") for f in files)
225
+ has_server_overrides = any(f.startswith("server-overrides/") for f in files)
226
+
227
+ if has_overrides:
228
+ console.print("[green]✅ overrides/ folder present[/green]")
229
+ else:
230
+ console.print("[dim]No overrides/ folder (optional)[/dim]")
231
+
232
+ if has_server_overrides:
233
+ console.print("[green]✅ server-overrides/ folder present[/green]")
234
+ else:
235
+ console.print("[dim]No server-overrides/ folder (optional)[/dim]")
215
236
 
216
237
  # Summary
217
238
  console.print("\n" + "=" * 60)
@@ -237,10 +258,10 @@ def validate(mrpack_file: str | None = None) -> None:
237
258
  console.print("[yellow]Run 'ModForge-CLI build' again to fix[/yellow]")
238
259
  raise typer.Exit(1)
239
260
 
240
- except zipfile.BadZipFile:
261
+ except zipfile.BadZipFile as e:
241
262
  console.print("[red]❌ ERROR: Not a valid ZIP/MRPACK file[/red]")
242
- raise typer.Exit(1)
263
+ raise typer.Exit(1) from e
243
264
  except json.JSONDecodeError as e:
244
265
  console.print("[red]❌ ERROR: Invalid JSON in modrinth.index.json[/red]")
245
266
  console.print(f"[dim]{e}[/dim]")
246
- raise typer.Exit(1)
267
+ raise typer.Exit(1) from e
@@ -5,6 +5,7 @@ from collections.abc import Iterable
5
5
  import hashlib
6
6
  import json
7
7
  from pathlib import Path
8
+ from pprint import pprint
8
9
 
9
10
  import aiohttp
10
11
  from rich.console import Console
@@ -15,6 +16,21 @@ from modforge_cli.api import ModrinthAPIConfig
15
16
  console = Console()
16
17
 
17
18
 
19
+ def validate_filename(filename: str) -> None:
20
+ """
21
+ Validate filename to prevent directory traversal attacks.
22
+
23
+ Per Modrinth spec: "make sure this field doesn't exit the Minecraft
24
+ instance directory for security reasons. To do this, make sure it
25
+ doesn't contain .. or start with a drive name"
26
+ """
27
+ if ".." in filename:
28
+ raise ValueError(f"Security: filename contains '..': {filename}")
29
+
30
+ if filename.startswith(("/", "\\", "\\\\")) or (len(filename) >= 2 and filename[1] == ":"):
31
+ raise ValueError(f"Security: filename is absolute path: {filename}")
32
+
33
+
18
34
  class ModDownloader:
19
35
  def __init__(
20
36
  self,
@@ -104,16 +120,19 @@ class ModDownloader:
104
120
 
105
121
  async def _download_project(self, project_id: str) -> None:
106
122
  # 1. Fetch all versions for this project
123
+ project_url = self.api.project(project_id)
107
124
  url = self.api.project_versions(project_id)
125
+ self.api.environments()
108
126
 
109
127
  try:
110
- async with self.session.get(url) as r:
111
- if r.status != 200:
128
+ async with self.session.get(url) as r, self.session.get(project_url) as rs:
129
+ if r.status != 200 or rs.status != 200:
112
130
  console.print(
113
131
  f"[red]Failed to fetch versions for {project_id}: HTTP {r.status}[/red]"
114
132
  )
115
133
  return
116
134
  versions = await r.json()
135
+ project = await rs.json()
117
136
  except Exception as e:
118
137
  console.print(f"[red]Error fetching {project_id}: {e}[/red]")
119
138
  return
@@ -146,6 +165,13 @@ class ModDownloader:
146
165
  )
147
166
  return
148
167
 
168
+ # SECURITY: Validate filename
169
+ try:
170
+ validate_filename(primary_file["filename"])
171
+ except ValueError as e:
172
+ console.print(f"[red]{e}[/red]")
173
+ return
174
+
149
175
  # 4. Download file to mods/ directory
150
176
  dest = self.output_dir / primary_file["filename"]
151
177
 
@@ -188,10 +214,29 @@ class ModDownloader:
188
214
  f" Got: {sha1}"
189
215
  )
190
216
 
191
- # 6. Register in index (Modrinth format)
217
+ # 6. Extract environment info from version
218
+ # Per Modrinth spec, env can be: required, optional, unsupported
219
+ client_side = project.get("client_side", "required")
220
+ server_side = project.get("server_side", "required")
221
+
222
+ # Normalize values (Modrinth API uses different terms)
223
+ env_map = {
224
+ "required": "required",
225
+ "optional": "optional",
226
+ "unsupported": "unsupported",
227
+ "unknown": "required", # Default to required if unknown
228
+ }
229
+
230
+ env = {
231
+ "client": env_map.get(client_side, "required"),
232
+ "server": env_map.get(server_side, "required"),
233
+ }
234
+
235
+ # 7. Register in index (Modrinth format)
192
236
  file_entry = {
193
237
  "path": f"mods/{primary_file['filename']}",
194
238
  "hashes": {"sha1": sha1, "sha512": sha512},
239
+ "env": env, # NEW: Environment specification
195
240
  "downloads": [primary_file["url"]],
196
241
  "fileSize": primary_file["size"],
197
242
  }
@@ -173,8 +173,12 @@ class ModResolver:
173
173
  if not dep_id:
174
174
  continue
175
175
 
176
- if dtype == "incompatible":
177
- raise RuntimeError(f"Incompatible dependency detected: {pid} ↔ {dep_id}")
176
+ if dtype == "incompatible" and dep_id in resolved:
177
+ resolved.remove(dep_id)
178
+ print(
179
+ f"Warning: Removed incompatible dependency '{dep_id}' "
180
+ f"(conflicts with '{pid}') — it may have been added earlier."
181
+ )
178
182
 
179
183
  if dtype in ("required", "optional") and dep_id not in resolved:
180
184
  resolved.add(dep_id)
@@ -0,0 +1,18 @@
1
+ import asyncio
2
+
3
+ from modforge_cli.api import ModrinthAPIConfig
4
+ from modforge_cli.core import get_api_session
5
+
6
+ api = ModrinthAPIConfig()
7
+
8
+ url = api.search("Fabric API", game_versions=["1.21.11"], loaders=["fabric"])
9
+
10
+ async def run() -> None:
11
+ async with await get_api_session() as session, await session.get(url) as res:
12
+ if res.status != 200:
13
+ print("Error on request")
14
+ return
15
+ data = await res.json()
16
+ print(data["hits"][0]["server_side"], data["hits"][0]["client_side"])
17
+
18
+ asyncio.run(run())
@@ -1,37 +0,0 @@
1
- # ModForge-CLI ⛏
2
-
3
- [![Python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue)](https://www.python.org/)
4
- [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
- [![Modrinth API v2](https://img.shields.io/badge/Modrinth-API%20v2-orange)](https://docs.modrinth.com/api-spec)
6
-
7
- **ModForge-CLI** is a powerful CLI tool for building and managing custom Minecraft modpacks using the Modrinth API v2.
8
-
9
- Search for projects, fetch versions, validate manifests, download mods with hash checks, and generate complete files — all from the terminal.
10
-
11
- Ideal for modpack developers, server admins, and automation scripts.
12
-
13
- ## Terminal Banner
14
-
15
- When you run ModForge-CLI, you'll be greeted with this colorful Minecraft-themed banner
16
-
17
- ## Key Features
18
-
19
- - **Modrinth API v2 Integration**: Search projects, list versions, fetch metadata in bulk.
20
- - **Modpack Management**: Read/validate `modrinth.index.json`, build packs from metadata.
21
- - **Validation**: Full JSON Schema checks + optional Pydantic models for strict typing.
22
-
23
- ## Installation
24
-
25
- Requires **Python 3.13+**.
26
-
27
- **Recommended (Poetry)**:
28
-
29
- ```bash
30
- poetry install
31
- ```
32
-
33
- **Alternative (pip)**:
34
-
35
- ```bash
36
- pip install -r requirements.txt
37
- ```
@@ -1,7 +0,0 @@
1
- """
2
- CLI package for ModForge-CLI
3
- """
4
-
5
- from . import main
6
-
7
- __all__ = ["main"]
File without changes