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.
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/PKG-INFO +54 -1
- modforge_cli-0.2.4/README.md +90 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/pyproject.toml +1 -1
- modforge_cli-0.2.4/src/modforge_cli/__init__.py +3 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/__main__.py +0 -5
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/__version__.py +1 -1
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/export.py +71 -50
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/downloader.py +48 -3
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/resolver.py +6 -2
- modforge_cli-0.2.4/src/modforge_cli/test.py +18 -0
- modforge_cli-0.2.3/README.md +0 -37
- modforge_cli-0.2.3/src/modforge_cli/__init__.py +0 -7
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/LICENSE +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/api/__init__.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/api/modrinth.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/__init__.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/modpack.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/project.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/setup.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/shared.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/sklauncher.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/cli/utils.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/__init__.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/models.py +0 -0
- {modforge_cli-0.2.3 → modforge_cli-0.2.4}/src/modforge_cli/core/policy.py +0 -0
- {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
|
+
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
|
+
[](https://www.python.org/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](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
|
+
```
|
|
@@ -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:
|
|
@@ -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
|
|
56
|
-
|
|
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]
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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())
|
modforge_cli-0.2.3/README.md
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# ModForge-CLI ⛏
|
|
2
|
-
|
|
3
|
-
[](https://www.python.org/)
|
|
4
|
-
[](LICENSE)
|
|
5
|
-
[](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
|
-
```
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|