modforge-cli 0.1.7__tar.gz → 0.1.9__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.1.7 → modforge_cli-0.1.9}/PKG-INFO +1 -1
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/pyproject.toml +2 -2
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/src/modforge_cli/__init__.py +0 -2
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/src/modforge_cli/__version__.py +1 -1
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/src/modforge_cli/api/__init__.py +1 -1
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/src/modforge_cli/api/modrinth.py +4 -12
- modforge_cli-0.1.9/src/modforge_cli/cli.py +525 -0
- modforge_cli-0.1.9/src/modforge_cli/core/__init__.py +39 -0
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/src/modforge_cli/core/downloader.py +5 -7
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/src/modforge_cli/core/models.py +16 -22
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/src/modforge_cli/core/policy.py +13 -14
- modforge_cli-0.1.9/src/modforge_cli/core/resolver.py +184 -0
- modforge_cli-0.1.9/src/modforge_cli/core/utils.py +262 -0
- modforge_cli-0.1.7/src/modforge_cli/cli.py +0 -431
- modforge_cli-0.1.7/src/modforge_cli/core/__init__.py +0 -23
- modforge_cli-0.1.7/src/modforge_cli/core/resolver.py +0 -134
- modforge_cli-0.1.7/src/modforge_cli/core/utils.py +0 -149
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/LICENSE +0 -0
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/README.md +0 -0
- {modforge_cli-0.1.7 → modforge_cli-0.1.9}/src/modforge_cli/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "modforge-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.9"
|
|
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" }
|
|
@@ -48,7 +48,7 @@ dev = [
|
|
|
48
48
|
package-mode = true
|
|
49
49
|
|
|
50
50
|
[tool.poetry.scripts]
|
|
51
|
-
modforge = "modforge_cli.__main__:main"
|
|
51
|
+
modforge-cli = "modforge_cli.__main__:main"
|
|
52
52
|
|
|
53
53
|
[project.urls]
|
|
54
54
|
Homepage = "https://frank1o3.github.io/ModForge-CLI/"
|
|
@@ -21,9 +21,7 @@ class ModrinthAPIConfig:
|
|
|
21
21
|
|
|
22
22
|
def _load_config(self) -> None:
|
|
23
23
|
if not self.config_path.exists():
|
|
24
|
-
raise FileNotFoundError(
|
|
25
|
-
f"Modrinth API config not found: {self.config_path}"
|
|
26
|
-
)
|
|
24
|
+
raise FileNotFoundError(f"Modrinth API config not found: {self.config_path}")
|
|
27
25
|
|
|
28
26
|
with open(self.config_path, "r", encoding="utf-8") as f:
|
|
29
27
|
data = json.load(f)
|
|
@@ -123,9 +121,7 @@ class ModrinthAPIConfig:
|
|
|
123
121
|
return self.build_url(self.endpoints["projects"]["project"], id=project_id)
|
|
124
122
|
|
|
125
123
|
def project_versions(self, project_id: str) -> str:
|
|
126
|
-
return self.build_url(
|
|
127
|
-
self.endpoints["projects"]["project_versions"], id=project_id
|
|
128
|
-
)
|
|
124
|
+
return self.build_url(self.endpoints["projects"]["project_versions"], id=project_id)
|
|
129
125
|
|
|
130
126
|
def project_dependencies(self, project_id: str) -> str:
|
|
131
127
|
return self.build_url(self.endpoints["projects"]["dependencies"], id=project_id)
|
|
@@ -137,9 +133,7 @@ class ModrinthAPIConfig:
|
|
|
137
133
|
return self.build_url(self.endpoints["projects"]["icon"], id=project_id)
|
|
138
134
|
|
|
139
135
|
def check_following(self, project_id: str) -> str:
|
|
140
|
-
return self.build_url(
|
|
141
|
-
self.endpoints["projects"]["check_following"], id=project_id
|
|
142
|
-
)
|
|
136
|
+
return self.build_url(self.endpoints["projects"]["check_following"], id=project_id)
|
|
143
137
|
|
|
144
138
|
# === Versions ===
|
|
145
139
|
|
|
@@ -158,9 +152,7 @@ class ModrinthAPIConfig:
|
|
|
158
152
|
return self.build_url(self.endpoints["versions"]["file_by_hash"], hash=hash_)
|
|
159
153
|
|
|
160
154
|
def versions_by_hash(self, hash_: str) -> str:
|
|
161
|
-
return self.build_url(
|
|
162
|
-
self.endpoints["versions"]["versions_by_hash"], hash=hash_
|
|
163
|
-
)
|
|
155
|
+
return self.build_url(self.endpoints["versions"]["versions_by_hash"], hash=hash_)
|
|
164
156
|
|
|
165
157
|
def latest_version_for_hash(self, hash_: str, algorithm: str = "sha1") -> str:
|
|
166
158
|
return self.build_url(
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
from pyfiglet import figlet_format
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.prompt import Confirm
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from rich.text import Text
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from modforge_cli.api import ModrinthAPIConfig
|
|
20
|
+
from modforge_cli.core import (
|
|
21
|
+
Manifest,
|
|
22
|
+
ModPolicy,
|
|
23
|
+
ModResolver,
|
|
24
|
+
ensure_config_file,
|
|
25
|
+
get_api_session,
|
|
26
|
+
get_manifest,
|
|
27
|
+
install_fabric,
|
|
28
|
+
load_registry,
|
|
29
|
+
perform_add,
|
|
30
|
+
run,
|
|
31
|
+
save_registry_atomic,
|
|
32
|
+
self_update,
|
|
33
|
+
setup_crash_logging,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Import version info
|
|
37
|
+
try:
|
|
38
|
+
from modforge_cli.__version__ import __author__, __version__
|
|
39
|
+
except ImportError:
|
|
40
|
+
__version__ = "unknown"
|
|
41
|
+
__author__ = "Frank1o3"
|
|
42
|
+
|
|
43
|
+
app = typer.Typer(
|
|
44
|
+
add_completion=False,
|
|
45
|
+
no_args_is_help=False,
|
|
46
|
+
)
|
|
47
|
+
console = Console()
|
|
48
|
+
|
|
49
|
+
# Configuration
|
|
50
|
+
FABRIC_LOADER_VERSION = "0.16.9"
|
|
51
|
+
CONFIG_PATH = Path.home() / ".config" / "ModForge-CLI"
|
|
52
|
+
REGISTRY_PATH = CONFIG_PATH / "registry.json"
|
|
53
|
+
MODRINTH_API = CONFIG_PATH / "modrinth_api.json"
|
|
54
|
+
POLICY_PATH = CONFIG_PATH / "policy.json"
|
|
55
|
+
|
|
56
|
+
# Use versioned URLs to prevent breaking changes
|
|
57
|
+
GITHUB_RAW = "https://raw.githubusercontent.com/Frank1o3/ModForge-CLI"
|
|
58
|
+
VERSION_TAG = "v0.1.8" # Update this with each release
|
|
59
|
+
|
|
60
|
+
FABRIC_INSTALLER_URL = (
|
|
61
|
+
"https://maven.fabricmc.net/net/fabricmc/fabric-installer/1.1.1/fabric-installer-1.1.1.jar"
|
|
62
|
+
)
|
|
63
|
+
FABRIC_INSTALLER_SHA256 = (
|
|
64
|
+
"8fa465768bd7fc452e08c3a1e5c8a6b4b5f6a4e64bc7def47f89d8d3a6f4e7b8" # Replace with actual hash
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
DEFAULT_MODRINTH_API_URL = f"{GITHUB_RAW}/{VERSION_TAG}/configs/modrinth_api.json"
|
|
68
|
+
DEFAULT_POLICY_URL = f"{GITHUB_RAW}/{VERSION_TAG}/configs/policy.json"
|
|
69
|
+
|
|
70
|
+
# Setup crash logging
|
|
71
|
+
LOG_DIR = setup_crash_logging()
|
|
72
|
+
|
|
73
|
+
# Ensure configs exist
|
|
74
|
+
ensure_config_file(MODRINTH_API, DEFAULT_MODRINTH_API_URL, "Modrinth API", console)
|
|
75
|
+
ensure_config_file(POLICY_PATH, DEFAULT_POLICY_URL, "Policy", console)
|
|
76
|
+
|
|
77
|
+
# Initialize API
|
|
78
|
+
api = ModrinthAPIConfig(MODRINTH_API)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def render_banner():
|
|
82
|
+
"""Renders a stylized banner"""
|
|
83
|
+
width = console.width
|
|
84
|
+
font = "slant" if width > 60 else "small"
|
|
85
|
+
|
|
86
|
+
ascii_art = figlet_format("ModForge-CLI", font=font)
|
|
87
|
+
banner_text = Text(ascii_art, style="bold cyan")
|
|
88
|
+
|
|
89
|
+
info_line = Text.assemble(
|
|
90
|
+
(" ⛏ ", "yellow"),
|
|
91
|
+
(f"v{__version__}", "bold white"),
|
|
92
|
+
(" | ", "dim"),
|
|
93
|
+
("Created by ", "italic white"),
|
|
94
|
+
(f"{__author__}", "bold magenta"),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
console.print(
|
|
98
|
+
Panel(
|
|
99
|
+
Text.assemble(banner_text, "\n", info_line),
|
|
100
|
+
border_style="blue",
|
|
101
|
+
padding=(1, 2),
|
|
102
|
+
expand=False,
|
|
103
|
+
),
|
|
104
|
+
justify="left",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.callback(invoke_without_command=True)
|
|
109
|
+
def main_callback(
|
|
110
|
+
ctx: typer.Context,
|
|
111
|
+
version: Optional[bool] = typer.Option(None, "--version", "-v", help="Show version and exit"),
|
|
112
|
+
verbose: Optional[bool] = typer.Option(None, "--verbose", help="Enable verbose logging"),
|
|
113
|
+
):
|
|
114
|
+
"""ModForge-CLI: A powerful Minecraft modpack manager for Modrinth."""
|
|
115
|
+
|
|
116
|
+
if verbose:
|
|
117
|
+
# Enable verbose logging
|
|
118
|
+
logging.basicConfig(
|
|
119
|
+
level=logging.DEBUG,
|
|
120
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
121
|
+
handlers=[
|
|
122
|
+
logging.FileHandler(LOG_DIR / f"modforge-{__version__}.log"),
|
|
123
|
+
logging.StreamHandler(),
|
|
124
|
+
],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if version:
|
|
128
|
+
console.print(f"ModForge-CLI Version: [bold cyan]{__version__}[/bold cyan]")
|
|
129
|
+
raise typer.Exit()
|
|
130
|
+
|
|
131
|
+
if ctx.invoked_subcommand is None:
|
|
132
|
+
render_banner()
|
|
133
|
+
console.print("\n[bold yellow]Usage:[/bold yellow] ModForge-CLI [COMMAND] [ARGS]...")
|
|
134
|
+
console.print("\n[bold cyan]Core Commands:[/bold cyan]")
|
|
135
|
+
console.print(" [green]setup[/green] Initialize a new modpack project")
|
|
136
|
+
console.print(" [green]ls[/green] List all registered projects")
|
|
137
|
+
console.print(" [green]add[/green] Add a mod/resource/shader to manifest")
|
|
138
|
+
console.print(" [green]resolve[/green] Resolve all dependencies")
|
|
139
|
+
console.print(" [green]build[/green] Download files and setup loader")
|
|
140
|
+
console.print(" [green]export[/green] Create the final .mrpack")
|
|
141
|
+
console.print(" [green]remove[/green] Remove a modpack project")
|
|
142
|
+
console.print("\n[bold cyan]Utility:[/bold cyan]")
|
|
143
|
+
console.print(" [green]self-update[/green] Update ModForge-CLI")
|
|
144
|
+
console.print(" [green]doctor[/green] Validate installation")
|
|
145
|
+
console.print("\nRun [white]ModForge-CLI --help[/white] for details.\n")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command()
|
|
149
|
+
def setup(
|
|
150
|
+
name: str,
|
|
151
|
+
mc: str = "1.21.1",
|
|
152
|
+
loader: str = "fabric",
|
|
153
|
+
loader_version: str = FABRIC_LOADER_VERSION,
|
|
154
|
+
):
|
|
155
|
+
"""Initialize a new modpack project"""
|
|
156
|
+
pack_dir = Path.cwd() / name
|
|
157
|
+
|
|
158
|
+
if pack_dir.exists():
|
|
159
|
+
console.print(f"[red]Error:[/red] Directory '{name}' already exists")
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
|
|
162
|
+
pack_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
|
|
164
|
+
# Create standard structure
|
|
165
|
+
for folder in [
|
|
166
|
+
"mods",
|
|
167
|
+
"overrides/resourcepacks",
|
|
168
|
+
"overrides/shaderpacks",
|
|
169
|
+
"overrides/config",
|
|
170
|
+
"overrides/config/openloader/data",
|
|
171
|
+
"versions",
|
|
172
|
+
]:
|
|
173
|
+
(pack_dir / folder).mkdir(parents=True, exist_ok=True)
|
|
174
|
+
|
|
175
|
+
# Create manifest
|
|
176
|
+
manifest = Manifest(name=name, minecraft=mc, loader=loader, loader_version=loader_version)
|
|
177
|
+
(pack_dir / "ModForge-CLI.json").write_text(manifest.model_dump_json(indent=4))
|
|
178
|
+
|
|
179
|
+
# Create Modrinth index
|
|
180
|
+
index_data = {
|
|
181
|
+
"formatVersion": 1,
|
|
182
|
+
"game": "minecraft",
|
|
183
|
+
"versionId": "1.0.0",
|
|
184
|
+
"name": name,
|
|
185
|
+
"dependencies": {"minecraft": mc, loader: "*"},
|
|
186
|
+
"files": [],
|
|
187
|
+
}
|
|
188
|
+
(pack_dir / "modrinth.index.json").write_text(json.dumps(index_data, indent=2))
|
|
189
|
+
|
|
190
|
+
# Register project
|
|
191
|
+
registry = load_registry(REGISTRY_PATH)
|
|
192
|
+
registry[name] = str(pack_dir.absolute())
|
|
193
|
+
save_registry_atomic(registry, REGISTRY_PATH)
|
|
194
|
+
|
|
195
|
+
console.print(f"[green]✓ Project '{name}' created at {pack_dir}[/green]")
|
|
196
|
+
console.print(f"[dim]Run 'cd {name}' to enter the project[/dim]")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.command()
|
|
200
|
+
def add(name: str, project_type: str = "mod", pack_name: Optional[str] = None):
|
|
201
|
+
"""Add a project to the manifest"""
|
|
202
|
+
|
|
203
|
+
if project_type not in ["mod", "resourcepack", "shaderpack"]:
|
|
204
|
+
console.print(f"[red]Invalid type:[/red] {project_type}")
|
|
205
|
+
console.print("[yellow]Valid types:[/yellow] mod, resourcepack, shaderpack")
|
|
206
|
+
raise typer.Exit(1)
|
|
207
|
+
|
|
208
|
+
# Auto-detect pack if not specified
|
|
209
|
+
if not pack_name:
|
|
210
|
+
manifest = get_manifest(console, Path.cwd())
|
|
211
|
+
if manifest:
|
|
212
|
+
pack_name = manifest.name
|
|
213
|
+
else:
|
|
214
|
+
console.print("[red]No manifest found in current directory[/red]")
|
|
215
|
+
console.print("[yellow]Specify --pack-name or run from project directory[/yellow]")
|
|
216
|
+
raise typer.Exit(1)
|
|
217
|
+
|
|
218
|
+
registry = load_registry(REGISTRY_PATH)
|
|
219
|
+
if pack_name not in registry:
|
|
220
|
+
console.print(f"[red]Pack '{pack_name}' not found in registry[/red]")
|
|
221
|
+
console.print("[yellow]Available packs:[/yellow]")
|
|
222
|
+
for p in registry.keys():
|
|
223
|
+
console.print(f" - {p}")
|
|
224
|
+
raise typer.Exit(1)
|
|
225
|
+
|
|
226
|
+
pack_path = Path(registry[pack_name])
|
|
227
|
+
manifest_file = pack_path / "ModForge-CLI.json"
|
|
228
|
+
|
|
229
|
+
manifest = get_manifest(console, pack_path)
|
|
230
|
+
if not manifest:
|
|
231
|
+
console.print(f"[red]Could not load manifest at {manifest_file}[/red]")
|
|
232
|
+
raise typer.Exit(1)
|
|
233
|
+
|
|
234
|
+
asyncio.run(perform_add(api, name, manifest, project_type, console, manifest_file))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@app.command()
|
|
238
|
+
def resolve(pack_name: Optional[str] = None):
|
|
239
|
+
"""Resolve all mod dependencies"""
|
|
240
|
+
|
|
241
|
+
# Auto-detect pack
|
|
242
|
+
if not pack_name:
|
|
243
|
+
manifest = get_manifest(console, Path.cwd())
|
|
244
|
+
if manifest:
|
|
245
|
+
pack_name = manifest.name
|
|
246
|
+
else:
|
|
247
|
+
console.print("[red]No manifest found[/red]")
|
|
248
|
+
raise typer.Exit(1)
|
|
249
|
+
|
|
250
|
+
registry = load_registry(REGISTRY_PATH)
|
|
251
|
+
if pack_name not in registry:
|
|
252
|
+
console.print(f"[red]Pack '{pack_name}' not found[/red]")
|
|
253
|
+
raise typer.Exit(1)
|
|
254
|
+
|
|
255
|
+
pack_path = Path(registry[pack_name])
|
|
256
|
+
manifest_file = pack_path / "ModForge-CLI.json"
|
|
257
|
+
|
|
258
|
+
manifest = get_manifest(console, pack_path)
|
|
259
|
+
if not manifest:
|
|
260
|
+
console.print(f"[red]Could not load manifest[/red]")
|
|
261
|
+
raise typer.Exit(1)
|
|
262
|
+
|
|
263
|
+
console.print(f"[cyan]Resolving dependencies for {pack_name}...[/cyan]")
|
|
264
|
+
|
|
265
|
+
policy = ModPolicy(POLICY_PATH)
|
|
266
|
+
resolver = ModResolver(
|
|
267
|
+
policy=policy, api=api, mc_version=manifest.minecraft, loader=manifest.loader
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
async def do_resolve():
|
|
271
|
+
async with await get_api_session() as session:
|
|
272
|
+
return await resolver.resolve(manifest.mods, session)
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
resolved_mods = asyncio.run(do_resolve())
|
|
276
|
+
except Exception as e:
|
|
277
|
+
console.print(f"[red]Resolution failed:[/red] {e}")
|
|
278
|
+
raise typer.Exit(1)
|
|
279
|
+
|
|
280
|
+
manifest.mods = sorted(list(resolved_mods))
|
|
281
|
+
manifest_file.write_text(manifest.model_dump_json(indent=4))
|
|
282
|
+
|
|
283
|
+
console.print(f"[green]✓ Resolved {len(manifest.mods)} mods[/green]")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@app.command()
|
|
287
|
+
def build(pack_name: Optional[str] = None):
|
|
288
|
+
"""Download all mods and dependencies"""
|
|
289
|
+
|
|
290
|
+
if not pack_name:
|
|
291
|
+
manifest = get_manifest(console, Path.cwd())
|
|
292
|
+
if manifest:
|
|
293
|
+
pack_name = manifest.name
|
|
294
|
+
else:
|
|
295
|
+
console.print("[red]No manifest found[/red]")
|
|
296
|
+
raise typer.Exit(1)
|
|
297
|
+
|
|
298
|
+
registry = load_registry(REGISTRY_PATH)
|
|
299
|
+
if pack_name not in registry:
|
|
300
|
+
console.print(f"[red]Pack '{pack_name}' not found[/red]")
|
|
301
|
+
raise typer.Exit(1)
|
|
302
|
+
|
|
303
|
+
pack_path = Path(registry[pack_name])
|
|
304
|
+
manifest = get_manifest(console, pack_path)
|
|
305
|
+
if not manifest:
|
|
306
|
+
raise typer.Exit(1)
|
|
307
|
+
|
|
308
|
+
pack_root = pack_path
|
|
309
|
+
mods_dir = pack_root / "mods"
|
|
310
|
+
index_file = pack_root / "modrinth.index.json"
|
|
311
|
+
|
|
312
|
+
mods_dir.mkdir(exist_ok=True)
|
|
313
|
+
|
|
314
|
+
console.print(f"[cyan]Building {manifest.name}...[/cyan]")
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
asyncio.run(run(api, manifest, mods_dir, index_file))
|
|
318
|
+
console.print("[green]✓ Build complete[/green]")
|
|
319
|
+
except Exception as e:
|
|
320
|
+
console.print(f"[red]Build failed:[/red] {e}")
|
|
321
|
+
raise typer.Exit(1)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@app.command()
|
|
325
|
+
def export(pack_name: Optional[str] = None):
|
|
326
|
+
"""Create final .mrpack file"""
|
|
327
|
+
|
|
328
|
+
if not pack_name:
|
|
329
|
+
manifest = get_manifest(console, Path.cwd())
|
|
330
|
+
if manifest:
|
|
331
|
+
pack_name = manifest.name
|
|
332
|
+
else:
|
|
333
|
+
console.print("[red]No manifest found[/red]")
|
|
334
|
+
raise typer.Exit(1)
|
|
335
|
+
|
|
336
|
+
registry = load_registry(REGISTRY_PATH)
|
|
337
|
+
if pack_name not in registry:
|
|
338
|
+
console.print(f"[red]Pack '{pack_name}' not found[/red]")
|
|
339
|
+
raise typer.Exit(1)
|
|
340
|
+
|
|
341
|
+
pack_path = Path(registry[pack_name])
|
|
342
|
+
manifest = get_manifest(console, pack_path)
|
|
343
|
+
if not manifest:
|
|
344
|
+
raise typer.Exit(1)
|
|
345
|
+
|
|
346
|
+
loader_version = manifest.loader_version or FABRIC_LOADER_VERSION
|
|
347
|
+
|
|
348
|
+
console.print("[cyan]Exporting modpack...[/cyan]")
|
|
349
|
+
|
|
350
|
+
mods_dir = pack_path / "mods"
|
|
351
|
+
if not mods_dir.exists() or not any(mods_dir.iterdir()):
|
|
352
|
+
console.print("[red]No mods found. Run 'ModForge-CLI build' first[/red]")
|
|
353
|
+
raise typer.Exit(1)
|
|
354
|
+
|
|
355
|
+
# Install loader if needed
|
|
356
|
+
if manifest.loader == "fabric":
|
|
357
|
+
installer = pack_path / ".fabric-installer.jar"
|
|
358
|
+
|
|
359
|
+
if not installer.exists():
|
|
360
|
+
console.print("[yellow]Downloading Fabric installer...[/yellow]")
|
|
361
|
+
|
|
362
|
+
urllib.request.urlretrieve(FABRIC_INSTALLER_URL, installer)
|
|
363
|
+
|
|
364
|
+
# Verify hash (security)
|
|
365
|
+
# Note: Update FABRIC_INSTALLER_SHA256 with actual hash
|
|
366
|
+
# actual_hash = hashlib.sha256(installer.read_bytes()).hexdigest()
|
|
367
|
+
# if actual_hash != FABRIC_INSTALLER_SHA256:
|
|
368
|
+
# console.print("[red]Installer hash mismatch![/red]")
|
|
369
|
+
# installer.unlink()
|
|
370
|
+
# raise typer.Exit(1)
|
|
371
|
+
|
|
372
|
+
console.print("[yellow]Installing Fabric...[/yellow]")
|
|
373
|
+
try:
|
|
374
|
+
install_fabric(
|
|
375
|
+
installer=installer,
|
|
376
|
+
mc_version=manifest.minecraft,
|
|
377
|
+
loader_version=loader_version,
|
|
378
|
+
game_dir=pack_path,
|
|
379
|
+
)
|
|
380
|
+
except RuntimeError as e:
|
|
381
|
+
console.print(f"[red]{e}[/red]")
|
|
382
|
+
raise typer.Exit(1)
|
|
383
|
+
|
|
384
|
+
# Update index
|
|
385
|
+
index_file = pack_path / "modrinth.index.json"
|
|
386
|
+
index = json.loads(index_file.read_text())
|
|
387
|
+
index["dependencies"]["fabric-loader"] = loader_version
|
|
388
|
+
index_file.write_text(json.dumps(index, indent=2))
|
|
389
|
+
|
|
390
|
+
installer.unlink(missing_ok=True)
|
|
391
|
+
|
|
392
|
+
# Create .mrpack
|
|
393
|
+
zip_path = pack_path.parent / f"{pack_name}.mrpack"
|
|
394
|
+
shutil.make_archive(
|
|
395
|
+
base_name=str(zip_path.with_suffix("")),
|
|
396
|
+
format="zip",
|
|
397
|
+
root_dir=pack_path,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Rename .zip to .mrpack
|
|
401
|
+
zip_file = pack_path.parent / f"{pack_name}.zip"
|
|
402
|
+
if zip_file.exists():
|
|
403
|
+
zip_file.rename(zip_path)
|
|
404
|
+
|
|
405
|
+
console.print(f"[green bold]✓ Exported to {zip_path}[/green bold]")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@app.command()
|
|
409
|
+
def remove(pack_name: str):
|
|
410
|
+
"""Remove a modpack and unregister it"""
|
|
411
|
+
registry = load_registry(REGISTRY_PATH)
|
|
412
|
+
|
|
413
|
+
if pack_name not in registry:
|
|
414
|
+
console.print(f"[red]Pack '{pack_name}' not found[/red]")
|
|
415
|
+
raise typer.Exit(1)
|
|
416
|
+
|
|
417
|
+
pack_path = Path(registry[pack_name])
|
|
418
|
+
|
|
419
|
+
console.print(
|
|
420
|
+
Panel.fit(
|
|
421
|
+
f"[bold red]This will permanently delete:[/bold red]\n\n"
|
|
422
|
+
f"[white]{pack_name}[/white]\n"
|
|
423
|
+
f"[dim]{pack_path}[/dim]",
|
|
424
|
+
title="⚠️ Destructive Action",
|
|
425
|
+
border_style="red",
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if not Confirm.ask("Are you sure?", default=False):
|
|
430
|
+
console.print("Aborted.")
|
|
431
|
+
raise typer.Exit()
|
|
432
|
+
|
|
433
|
+
# Remove directory
|
|
434
|
+
if pack_path.exists():
|
|
435
|
+
shutil.rmtree(pack_path)
|
|
436
|
+
|
|
437
|
+
# Update registry
|
|
438
|
+
del registry[pack_name]
|
|
439
|
+
save_registry_atomic(registry, REGISTRY_PATH)
|
|
440
|
+
|
|
441
|
+
console.print(f"[green]✓ Removed {pack_name}[/green]")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@app.command(name="ls")
|
|
445
|
+
def list_projects():
|
|
446
|
+
"""List all registered modpacks"""
|
|
447
|
+
registry = load_registry(REGISTRY_PATH)
|
|
448
|
+
|
|
449
|
+
if not registry:
|
|
450
|
+
console.print("[yellow]No projects registered yet[/yellow]")
|
|
451
|
+
console.print("[dim]Run 'ModForge-CLI setup <name>' to create one[/dim]")
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
table = Table(title="ModForge-CLI Projects", header_style="bold magenta")
|
|
455
|
+
table.add_column("Name", style="cyan")
|
|
456
|
+
table.add_column("Location", style="dim")
|
|
457
|
+
|
|
458
|
+
for name, path in registry.items():
|
|
459
|
+
table.add_row(name, path)
|
|
460
|
+
|
|
461
|
+
console.print(table)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@app.command()
|
|
465
|
+
def doctor():
|
|
466
|
+
"""Validate ModForge-CLI installation"""
|
|
467
|
+
console.print("[bold cyan]Running diagnostics...[/bold cyan]\n")
|
|
468
|
+
|
|
469
|
+
issues = []
|
|
470
|
+
|
|
471
|
+
# Check Python version
|
|
472
|
+
|
|
473
|
+
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
474
|
+
if sys.version_info >= (3, 10):
|
|
475
|
+
console.print(f"[green]✓[/green] Python {py_version}")
|
|
476
|
+
else:
|
|
477
|
+
console.print(f"[red]✗[/red] Python {py_version} (requires 3.10+)")
|
|
478
|
+
issues.append("Upgrade Python")
|
|
479
|
+
|
|
480
|
+
# Check config files
|
|
481
|
+
for name, path in [("API Config", MODRINTH_API), ("Policy", POLICY_PATH)]:
|
|
482
|
+
if path.exists():
|
|
483
|
+
console.print(f"[green]✓[/green] {name}: {path}")
|
|
484
|
+
else:
|
|
485
|
+
console.print(f"[red]✗[/red] {name} missing")
|
|
486
|
+
issues.append(f"Reinstall {name}")
|
|
487
|
+
|
|
488
|
+
# Check registry
|
|
489
|
+
registry = load_registry(REGISTRY_PATH)
|
|
490
|
+
console.print(f"[green]✓[/green] Registry: {len(registry)} projects")
|
|
491
|
+
|
|
492
|
+
# Check Java
|
|
493
|
+
try:
|
|
494
|
+
result = subprocess.run(["java", "-version"], capture_output=True, text=True, check=True)
|
|
495
|
+
console.print("[green]✓[/green] Java installed")
|
|
496
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
497
|
+
console.print("[yellow]![/yellow] Java not found (needed for Fabric)")
|
|
498
|
+
issues.append("Install Java 17+")
|
|
499
|
+
|
|
500
|
+
# Summary
|
|
501
|
+
console.print()
|
|
502
|
+
if issues:
|
|
503
|
+
console.print("[yellow]Issues found:[/yellow]")
|
|
504
|
+
for issue in issues:
|
|
505
|
+
console.print(f" - {issue}")
|
|
506
|
+
else:
|
|
507
|
+
console.print("[green bold]✓ All checks passed![/green bold]")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@app.command(name="self-update")
|
|
511
|
+
def self_update_cmd():
|
|
512
|
+
"""Update ModForge-CLI to latest version"""
|
|
513
|
+
try:
|
|
514
|
+
self_update(console)
|
|
515
|
+
except Exception as e:
|
|
516
|
+
console.print(f"[red]Update failed:[/red] {e}")
|
|
517
|
+
raise typer.Exit(1)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def main():
|
|
521
|
+
app()
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
if __name__ == "__main__":
|
|
525
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from .downloader import ModDownloader
|
|
2
|
+
from .models import Hit, Manifest, ProjectVersion, ProjectVersionList, SearchResult
|
|
3
|
+
from .policy import ModPolicy
|
|
4
|
+
from .resolver import ModResolver
|
|
5
|
+
from .utils import (
|
|
6
|
+
detect_install_method,
|
|
7
|
+
ensure_config_file,
|
|
8
|
+
get_api_session,
|
|
9
|
+
get_manifest,
|
|
10
|
+
install_fabric,
|
|
11
|
+
load_registry,
|
|
12
|
+
perform_add,
|
|
13
|
+
run,
|
|
14
|
+
save_registry_atomic,
|
|
15
|
+
self_update,
|
|
16
|
+
setup_crash_logging,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ModPolicy",
|
|
21
|
+
"ModResolver",
|
|
22
|
+
"Manifest",
|
|
23
|
+
"Hit",
|
|
24
|
+
"SearchResult",
|
|
25
|
+
"ProjectVersion",
|
|
26
|
+
"ProjectVersionList",
|
|
27
|
+
"ModDownloader",
|
|
28
|
+
"ensure_config_file",
|
|
29
|
+
"install_fabric",
|
|
30
|
+
"run",
|
|
31
|
+
"get_api_session",
|
|
32
|
+
"get_manifest",
|
|
33
|
+
"self_update",
|
|
34
|
+
"perform_add",
|
|
35
|
+
"detect_install_method",
|
|
36
|
+
"load_registry",
|
|
37
|
+
"save_registry_atomic",
|
|
38
|
+
"setup_crash_logging",
|
|
39
|
+
]
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
from collections.abc import Iterable
|
|
4
5
|
import hashlib
|
|
5
6
|
import json
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import Iterable
|
|
8
8
|
|
|
9
9
|
import aiohttp
|
|
10
10
|
from rich.console import Console
|
|
11
|
-
from rich.progress import Progress, SpinnerColumn,
|
|
11
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn
|
|
12
12
|
|
|
13
13
|
from modforge_cli.api import ModrinthAPIConfig
|
|
14
14
|
|
|
@@ -34,7 +34,7 @@ class ModDownloader:
|
|
|
34
34
|
|
|
35
35
|
self.index = json.loads(index_file.read_text())
|
|
36
36
|
|
|
37
|
-
async def download_all(self, project_ids: Iterable[str]):
|
|
37
|
+
async def download_all(self, project_ids: Iterable[str]) -> None:
|
|
38
38
|
tasks = [self._download_project(pid) for pid in project_ids]
|
|
39
39
|
|
|
40
40
|
with Progress(
|
|
@@ -51,11 +51,9 @@ class ModDownloader:
|
|
|
51
51
|
|
|
52
52
|
self.index_file.write_text(json.dumps(self.index, indent=2))
|
|
53
53
|
|
|
54
|
-
async def _download_project(self, project_id: str):
|
|
54
|
+
async def _download_project(self, project_id: str) -> None:
|
|
55
55
|
# 1. Fetch compatible version
|
|
56
|
-
url = self.api.project_versions(
|
|
57
|
-
project_id
|
|
58
|
-
)
|
|
56
|
+
url = self.api.project_versions(project_id)
|
|
59
57
|
|
|
60
58
|
async with self.session.get(url) as r:
|
|
61
59
|
versions = await r.json()
|