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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modforge-cli
3
- Version: 0.1.7
3
+ Version: 0.1.9
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.1.7"
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/"
@@ -1,5 +1,3 @@
1
1
  """
2
2
  ModForge-CLI - A CLI tool for building and managing Minecraft modpacks
3
3
  """
4
-
5
- __version__ = "0.1.0"
@@ -1,5 +1,5 @@
1
1
  """
2
2
  Auto-generated file. DO NOT EDIT.
3
3
  """
4
- __version__ = "0.1.7"
4
+ __version__ = "0.1.9"
5
5
  __author__ = "Frank1o3"
@@ -4,4 +4,4 @@ api package - Exposes the Modrinth API client globally.
4
4
 
5
5
  from .modrinth import ModrinthAPIConfig
6
6
 
7
- __all__ = ["ModrinthAPIConfig"]
7
+ __all__ = ["ModrinthAPIConfig"]
@@ -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, BarColumn, TextColumn
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()