modforge-cli 0.2.1__tar.gz → 0.2.2__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.1 → modforge_cli-0.2.2}/PKG-INFO +1 -1
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/pyproject.toml +1 -1
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/__version__.py +1 -1
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/cli.py +344 -70
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/core/downloader.py +6 -8
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/LICENSE +0 -0
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/README.md +0 -0
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/__init__.py +0 -0
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/__main__.py +0 -0
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/api/__init__.py +0 -0
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/api/modrinth.py +0 -0
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/core/__init__.py +0 -0
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/core/models.py +0 -0
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/core/policy.py +0 -0
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/core/resolver.py +0 -0
- {modforge_cli-0.2.1 → modforge_cli-0.2.2}/src/modforge_cli/core/utils.py +0 -0
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
-
from pathlib import Path
|
|
4
3
|
import shutil
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import tempfile
|
|
8
|
+
from zipfile import ZipFile, ZIP_DEFLATED
|
|
8
9
|
|
|
10
|
+
import typer
|
|
9
11
|
from pyfiglet import figlet_format
|
|
10
12
|
from rich.console import Console
|
|
11
13
|
from rich.panel import Panel
|
|
12
14
|
from rich.prompt import Confirm
|
|
13
15
|
from rich.table import Table
|
|
14
16
|
from rich.text import Text
|
|
15
|
-
import typer
|
|
16
17
|
|
|
17
18
|
from modforge_cli.api import ModrinthAPIConfig
|
|
19
|
+
from modforge_cli.core import Manifest
|
|
20
|
+
from modforge_cli.core import ModPolicy, ModResolver
|
|
18
21
|
from modforge_cli.core import (
|
|
19
|
-
|
|
20
|
-
ModPolicy,
|
|
21
|
-
ModResolver,
|
|
22
|
-
ensure_config_file,
|
|
23
|
-
get_api_session,
|
|
24
|
-
get_manifest,
|
|
22
|
+
self_update,
|
|
25
23
|
install_fabric,
|
|
26
|
-
|
|
24
|
+
get_manifest,
|
|
27
25
|
perform_add,
|
|
26
|
+
ensure_config_file,
|
|
28
27
|
run,
|
|
29
28
|
save_registry_atomic,
|
|
30
|
-
|
|
29
|
+
load_registry,
|
|
31
30
|
setup_crash_logging,
|
|
31
|
+
get_api_session,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
# Import version info
|
|
@@ -113,7 +113,6 @@ def main_callback(
|
|
|
113
113
|
|
|
114
114
|
if verbose:
|
|
115
115
|
# Enable verbose logging
|
|
116
|
-
import logging
|
|
117
116
|
|
|
118
117
|
logging.basicConfig(
|
|
119
118
|
level=logging.DEBUG,
|
|
@@ -132,16 +131,18 @@ def main_callback(
|
|
|
132
131
|
render_banner()
|
|
133
132
|
console.print("\n[bold yellow]Usage:[/bold yellow] ModForge-CLI [COMMAND] [ARGS]...")
|
|
134
133
|
console.print("\n[bold cyan]Core Commands:[/bold cyan]")
|
|
135
|
-
console.print(" [green]setup[/green]
|
|
136
|
-
console.print(" [green]ls[/green]
|
|
137
|
-
console.print(" [green]add[/green]
|
|
138
|
-
console.print(" [green]resolve[/green]
|
|
139
|
-
console.print(" [green]build[/green]
|
|
140
|
-
console.print(" [green]export[/green]
|
|
141
|
-
console.print(" [green]
|
|
134
|
+
console.print(" [green]setup[/green] Initialize a new modpack project")
|
|
135
|
+
console.print(" [green]ls[/green] List all registered projects")
|
|
136
|
+
console.print(" [green]add[/green] Add a mod/resource/shader to manifest")
|
|
137
|
+
console.print(" [green]resolve[/green] Resolve all dependencies")
|
|
138
|
+
console.print(" [green]build[/green] Download files and setup loader")
|
|
139
|
+
console.print(" [green]export[/green] Create the final .mrpack")
|
|
140
|
+
console.print(" [green]validate[/green] Check .mrpack for issues")
|
|
141
|
+
console.print(" [green]sklauncher[/green] Create SKLauncher profile (no .mrpack)")
|
|
142
|
+
console.print(" [green]remove[/green] Remove a modpack project")
|
|
142
143
|
console.print("\n[bold cyan]Utility:[/bold cyan]")
|
|
143
|
-
console.print(" [green]self-update[/green]
|
|
144
|
-
console.print(" [green]doctor[/green]
|
|
144
|
+
console.print(" [green]self-update[/green] Update ModForge-CLI")
|
|
145
|
+
console.print(" [green]doctor[/green] Validate installation")
|
|
145
146
|
console.print("\nRun [white]ModForge-CLI --help[/white] for details.\n")
|
|
146
147
|
|
|
147
148
|
|
|
@@ -186,12 +187,13 @@ def setup(
|
|
|
186
187
|
}
|
|
187
188
|
loader_key = loader_key_map.get(loader.lower(), loader.lower())
|
|
188
189
|
|
|
190
|
+
# SKLauncher requires exact format - dependencies MUST have loader first
|
|
189
191
|
index_data = {
|
|
190
192
|
"formatVersion": 1,
|
|
191
193
|
"game": "minecraft",
|
|
192
194
|
"versionId": "1.0.0",
|
|
193
195
|
"name": name,
|
|
194
|
-
"files": [],
|
|
196
|
+
"files": [],
|
|
195
197
|
"dependencies": {loader_key: loader_version, "minecraft": mc},
|
|
196
198
|
}
|
|
197
199
|
(pack_dir / "modrinth.index.json").write_text(json.dumps(index_data, indent=2))
|
|
@@ -352,61 +354,71 @@ def export(pack_name: str | None = None) -> None:
|
|
|
352
354
|
if not manifest:
|
|
353
355
|
raise typer.Exit(1)
|
|
354
356
|
|
|
355
|
-
loader_version = manifest.loader_version or FABRIC_LOADER_VERSION
|
|
356
|
-
|
|
357
357
|
console.print("[cyan]Exporting modpack...[/cyan]")
|
|
358
358
|
|
|
359
359
|
mods_dir = pack_path / "mods"
|
|
360
|
+
index_file = pack_path / "modrinth.index.json"
|
|
361
|
+
|
|
360
362
|
if not mods_dir.exists() or not any(mods_dir.iterdir()):
|
|
361
363
|
console.print("[red]No mods found. Run 'ModForge-CLI build' first[/red]")
|
|
362
364
|
raise typer.Exit(1)
|
|
363
365
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
# raise typer.Exit(1)
|
|
380
|
-
|
|
381
|
-
console.print("[yellow]Installing Fabric...[/yellow]")
|
|
382
|
-
try:
|
|
383
|
-
install_fabric(
|
|
384
|
-
installer=installer,
|
|
385
|
-
mc_version=manifest.minecraft,
|
|
386
|
-
loader_version=loader_version,
|
|
387
|
-
game_dir=pack_path,
|
|
388
|
-
)
|
|
389
|
-
console.print(f"[green]✓ Fabric {loader_version} installed[/green]")
|
|
390
|
-
except RuntimeError as e:
|
|
391
|
-
console.print(f"[red]{e}[/red]")
|
|
392
|
-
raise typer.Exit(1) from e
|
|
393
|
-
|
|
394
|
-
installer.unlink(missing_ok=True)
|
|
395
|
-
|
|
396
|
-
# Create .mrpack
|
|
397
|
-
zip_path = pack_path.parent / f"{pack_name}.mrpack"
|
|
398
|
-
shutil.make_archive(
|
|
399
|
-
base_name=str(zip_path.with_suffix("")),
|
|
400
|
-
format="zip",
|
|
401
|
-
root_dir=pack_path,
|
|
402
|
-
)
|
|
366
|
+
if not index_file.exists():
|
|
367
|
+
console.print("[red]No modrinth.index.json found[/red]")
|
|
368
|
+
raise typer.Exit(1)
|
|
369
|
+
|
|
370
|
+
# Validate index has files
|
|
371
|
+
index_data = json.loads(index_file.read_text())
|
|
372
|
+
if not index_data.get("files"):
|
|
373
|
+
console.print("[yellow]Warning: No files registered in index[/yellow]")
|
|
374
|
+
console.print("[yellow]This might cause issues. Run 'ModForge-CLI build' again.[/yellow]")
|
|
375
|
+
|
|
376
|
+
# Create .mrpack (which is just a renamed .zip)
|
|
377
|
+
|
|
378
|
+
# Create temp directory for packing
|
|
379
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
380
|
+
tmp_path = Path(tmpdir)
|
|
403
381
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if zip_file.exists():
|
|
407
|
-
zip_file.rename(zip_path)
|
|
382
|
+
# Copy modrinth.index.json to root
|
|
383
|
+
import shutil
|
|
408
384
|
|
|
409
|
-
|
|
385
|
+
shutil.copy2(index_file, tmp_path / "modrinth.index.json")
|
|
386
|
+
|
|
387
|
+
# Copy overrides if they exist
|
|
388
|
+
overrides_src = pack_path / "overrides"
|
|
389
|
+
if overrides_src.exists():
|
|
390
|
+
overrides_dst = tmp_path / "overrides"
|
|
391
|
+
shutil.copytree(overrides_src, overrides_dst)
|
|
392
|
+
console.print("[green]✓ Copied overrides[/green]")
|
|
393
|
+
|
|
394
|
+
# Create .mrpack
|
|
395
|
+
mrpack_path = pack_path.parent / f"{pack_name}.mrpack"
|
|
396
|
+
|
|
397
|
+
with ZipFile(mrpack_path, "w", ZIP_DEFLATED) as zipf:
|
|
398
|
+
# Add modrinth.index.json at root
|
|
399
|
+
zipf.write(tmp_path / "modrinth.index.json", "modrinth.index.json")
|
|
400
|
+
|
|
401
|
+
# Add overrides folder if exists
|
|
402
|
+
if overrides_src.exists():
|
|
403
|
+
for file_path in (tmp_path / "overrides").rglob("*"):
|
|
404
|
+
if file_path.is_file():
|
|
405
|
+
arcname = str(file_path.relative_to(tmp_path))
|
|
406
|
+
zipf.write(file_path, arcname)
|
|
407
|
+
|
|
408
|
+
console.print(f"[green bold]✓ Exported to {mrpack_path}[/green bold]")
|
|
409
|
+
|
|
410
|
+
# Show summary
|
|
411
|
+
file_count = len(index_data.get("files", []))
|
|
412
|
+
console.print("\n[cyan]Summary:[/cyan]")
|
|
413
|
+
console.print(f" Files registered: {file_count}")
|
|
414
|
+
console.print(f" Minecraft: {index_data['dependencies'].get('minecraft')}")
|
|
415
|
+
|
|
416
|
+
# Show loader
|
|
417
|
+
for loader in ["fabric-loader", "quilt-loader", "forge", "neoforge"]:
|
|
418
|
+
if loader in index_data["dependencies"]:
|
|
419
|
+
console.print(f" Loader: {loader} {index_data['dependencies'][loader]}")
|
|
420
|
+
|
|
421
|
+
console.print("\n[dim]Import this in SKLauncher, Prism, ATLauncher, etc.[/dim]")
|
|
410
422
|
|
|
411
423
|
|
|
412
424
|
@app.command()
|
|
@@ -473,6 +485,7 @@ def doctor() -> None:
|
|
|
473
485
|
issues = []
|
|
474
486
|
|
|
475
487
|
# Check Python version
|
|
488
|
+
import sys
|
|
476
489
|
|
|
477
490
|
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
|
478
491
|
console.print(f"[green]✓[/green] Python {py_version}")
|
|
@@ -491,6 +504,8 @@ def doctor() -> None:
|
|
|
491
504
|
|
|
492
505
|
# Check Java
|
|
493
506
|
try:
|
|
507
|
+
import subprocess
|
|
508
|
+
|
|
494
509
|
result = subprocess.run(["java", "-version"], capture_output=True, text=True, check=True)
|
|
495
510
|
console.print("[green]✓[/green] Java installed")
|
|
496
511
|
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
@@ -517,6 +532,265 @@ def self_update_cmd() -> None:
|
|
|
517
532
|
raise typer.Exit(1) from e
|
|
518
533
|
|
|
519
534
|
|
|
535
|
+
@app.command()
|
|
536
|
+
def validate(mrpack_file: str | None = None) -> None:
|
|
537
|
+
"""Validate .mrpack file for launcher compatibility"""
|
|
538
|
+
|
|
539
|
+
if not mrpack_file:
|
|
540
|
+
# Look for .mrpack in current directory
|
|
541
|
+
mrpacks = list(Path.cwd().glob("*.mrpack"))
|
|
542
|
+
if not mrpacks:
|
|
543
|
+
console.print("[red]No .mrpack file found in current directory[/red]")
|
|
544
|
+
console.print("[yellow]Usage: ModForge-CLI validate <file.mrpack>[/yellow]")
|
|
545
|
+
raise typer.Exit(1)
|
|
546
|
+
mrpack_path = mrpacks[0]
|
|
547
|
+
else:
|
|
548
|
+
mrpack_path = Path(mrpack_file)
|
|
549
|
+
|
|
550
|
+
if not mrpack_path.exists():
|
|
551
|
+
console.print(f"[red]File not found: {mrpack_path}[/red]")
|
|
552
|
+
raise typer.Exit(1)
|
|
553
|
+
|
|
554
|
+
console.print(f"[cyan]Validating {mrpack_path.name}...[/cyan]\n")
|
|
555
|
+
|
|
556
|
+
import zipfile
|
|
557
|
+
|
|
558
|
+
issues = []
|
|
559
|
+
warnings = []
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
with zipfile.ZipFile(mrpack_path, "r") as z:
|
|
563
|
+
files = z.namelist()
|
|
564
|
+
|
|
565
|
+
# Check for modrinth.index.json
|
|
566
|
+
if "modrinth.index.json" not in files:
|
|
567
|
+
console.print("[red]❌ CRITICAL: modrinth.index.json not found at root[/red]")
|
|
568
|
+
raise typer.Exit(1)
|
|
569
|
+
|
|
570
|
+
console.print("[green]✅ modrinth.index.json found[/green]")
|
|
571
|
+
|
|
572
|
+
# Read and validate index
|
|
573
|
+
index_data = json.loads(z.read("modrinth.index.json"))
|
|
574
|
+
|
|
575
|
+
# Check required fields
|
|
576
|
+
required = ["formatVersion", "game", "versionId", "name", "dependencies"]
|
|
577
|
+
for field in required:
|
|
578
|
+
if field not in index_data:
|
|
579
|
+
issues.append(f"Missing required field: {field}")
|
|
580
|
+
console.print(f"[red]❌ Missing: {field}[/red]")
|
|
581
|
+
else:
|
|
582
|
+
value = index_data[field]
|
|
583
|
+
if isinstance(value, dict):
|
|
584
|
+
console.print(f"[green]✅ {field}[/green]")
|
|
585
|
+
else:
|
|
586
|
+
console.print(f"[green]✅ {field}: {value}[/green]")
|
|
587
|
+
|
|
588
|
+
# Check dependencies
|
|
589
|
+
deps = index_data.get("dependencies", {})
|
|
590
|
+
if "minecraft" not in deps:
|
|
591
|
+
issues.append("Missing minecraft in dependencies")
|
|
592
|
+
console.print("[red]❌ Missing: minecraft version[/red]")
|
|
593
|
+
else:
|
|
594
|
+
console.print(f"[green]✅ Minecraft: {deps['minecraft']}[/green]")
|
|
595
|
+
|
|
596
|
+
# Check for loader
|
|
597
|
+
loaders = ["fabric-loader", "quilt-loader", "forge", "neoforge"]
|
|
598
|
+
has_loader = any(l in deps for l in loaders)
|
|
599
|
+
|
|
600
|
+
if not has_loader:
|
|
601
|
+
issues.append("No mod loader in dependencies")
|
|
602
|
+
console.print("[red]❌ Missing mod loader[/red]")
|
|
603
|
+
else:
|
|
604
|
+
for loader in loaders:
|
|
605
|
+
if loader in deps:
|
|
606
|
+
console.print(f"[green]✅ Loader: {loader} = {deps[loader]}[/green]")
|
|
607
|
+
|
|
608
|
+
# Check files array
|
|
609
|
+
files_list = index_data.get("files", [])
|
|
610
|
+
console.print(f"\n[cyan]📦 Files registered: {len(files_list)}[/cyan]")
|
|
611
|
+
|
|
612
|
+
if len(files_list) == 0:
|
|
613
|
+
warnings.append("No files in array (pack might not work)")
|
|
614
|
+
console.print("[yellow]⚠️ WARNING: files array is empty[/yellow]")
|
|
615
|
+
else:
|
|
616
|
+
# Check first file structure
|
|
617
|
+
sample = files_list[0]
|
|
618
|
+
file_required = ["path", "hashes", "downloads", "fileSize"]
|
|
619
|
+
|
|
620
|
+
missing_fields = [f for f in file_required if f not in sample]
|
|
621
|
+
if missing_fields:
|
|
622
|
+
issues.append(f"Files missing fields: {missing_fields}")
|
|
623
|
+
console.print(f"[red]❌ Files missing: {', '.join(missing_fields)}[/red]")
|
|
624
|
+
else:
|
|
625
|
+
console.print("[green]✅ File structure looks good[/green]")
|
|
626
|
+
|
|
627
|
+
# Check hashes
|
|
628
|
+
if "hashes" in sample:
|
|
629
|
+
if "sha1" not in sample["hashes"]:
|
|
630
|
+
issues.append("Files missing sha1 hash")
|
|
631
|
+
console.print("[red]❌ Missing sha1 hashes[/red]")
|
|
632
|
+
else:
|
|
633
|
+
console.print("[green]✅ sha1 hashes present[/green]")
|
|
634
|
+
|
|
635
|
+
if "sha512" not in sample["hashes"]:
|
|
636
|
+
warnings.append("Files missing sha512 hash")
|
|
637
|
+
console.print("[yellow]⚠️ Missing sha512 hashes (optional)[/yellow]")
|
|
638
|
+
else:
|
|
639
|
+
console.print("[green]✅ sha512 hashes present[/green]")
|
|
640
|
+
|
|
641
|
+
# Check env field
|
|
642
|
+
if "env" not in sample:
|
|
643
|
+
warnings.append("Files missing env field")
|
|
644
|
+
console.print("[yellow]⚠️ Missing env field (recommended)[/yellow]")
|
|
645
|
+
else:
|
|
646
|
+
console.print("[green]✅ env field present[/green]")
|
|
647
|
+
|
|
648
|
+
# Summary
|
|
649
|
+
console.print("\n" + "=" * 60)
|
|
650
|
+
|
|
651
|
+
if issues:
|
|
652
|
+
console.print(f"\n[red bold]❌ CRITICAL ISSUES ({len(issues)}):[/red bold]")
|
|
653
|
+
for issue in issues:
|
|
654
|
+
console.print(f" [red]• {issue}[/red]")
|
|
655
|
+
|
|
656
|
+
if warnings:
|
|
657
|
+
console.print(f"\n[yellow bold]⚠️ WARNINGS ({len(warnings)}):[/yellow bold]")
|
|
658
|
+
for warning in warnings:
|
|
659
|
+
console.print(f" [yellow]• {warning}[/yellow]")
|
|
660
|
+
|
|
661
|
+
if not issues and not warnings:
|
|
662
|
+
console.print("\n[green bold]✅ All checks passed![/green bold]")
|
|
663
|
+
console.print("[dim]Pack should work in all Modrinth-compatible launchers[/dim]")
|
|
664
|
+
elif not issues:
|
|
665
|
+
console.print("\n[green]✅ No critical issues[/green]")
|
|
666
|
+
console.print("[dim]Pack should work, but consider addressing warnings[/dim]")
|
|
667
|
+
else:
|
|
668
|
+
console.print("\n[red bold]❌ Pack has critical issues[/red bold]")
|
|
669
|
+
console.print("[yellow]Run 'ModForge-CLI build' again to fix[/yellow]")
|
|
670
|
+
raise typer.Exit(1)
|
|
671
|
+
|
|
672
|
+
except zipfile.BadZipFile:
|
|
673
|
+
console.print("[red]❌ ERROR: Not a valid ZIP/MRPACK file[/red]")
|
|
674
|
+
raise typer.Exit(1) from e
|
|
675
|
+
except json.JSONDecodeError as e:
|
|
676
|
+
console.print("[red]❌ ERROR: Invalid JSON in modrinth.index.json[/red]")
|
|
677
|
+
console.print(f"[dim]{e}[/dim]")
|
|
678
|
+
raise typer.Exit(1) from e
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@app.command()
|
|
682
|
+
def sklauncher(pack_name: str | None = None, profile_name: str | None = None) -> None:
|
|
683
|
+
"""Create SKLauncher-compatible profile (alternative to export)"""
|
|
684
|
+
|
|
685
|
+
if not pack_name:
|
|
686
|
+
manifest = get_manifest(console, Path.cwd())
|
|
687
|
+
if manifest:
|
|
688
|
+
pack_name = manifest.name
|
|
689
|
+
else:
|
|
690
|
+
console.print("[red]No manifest found[/red]")
|
|
691
|
+
raise typer.Exit(1)
|
|
692
|
+
|
|
693
|
+
registry = load_registry(REGISTRY_PATH)
|
|
694
|
+
if pack_name not in registry:
|
|
695
|
+
console.print(f"[red]Pack '{pack_name}' not found[/red]")
|
|
696
|
+
raise typer.Exit(1)
|
|
697
|
+
|
|
698
|
+
pack_path = Path(registry[pack_name])
|
|
699
|
+
manifest = get_manifest(console, pack_path)
|
|
700
|
+
if not manifest:
|
|
701
|
+
raise typer.Exit(1)
|
|
702
|
+
|
|
703
|
+
# Check if mods are built
|
|
704
|
+
mods_dir = pack_path / "mods"
|
|
705
|
+
if not mods_dir.exists() or not any(mods_dir.iterdir()):
|
|
706
|
+
console.print("[red]No mods found. Run 'ModForge-CLI build' first[/red]")
|
|
707
|
+
raise typer.Exit(1)
|
|
708
|
+
|
|
709
|
+
# Get Minecraft directory
|
|
710
|
+
import platform
|
|
711
|
+
|
|
712
|
+
if platform.system() == "Windows":
|
|
713
|
+
minecraft_dir = Path.home() / "AppData" / "Roaming" / ".minecraft"
|
|
714
|
+
elif platform.system() == "Darwin":
|
|
715
|
+
minecraft_dir = Path.home() / "Library" / "Application Support" / "minecraft"
|
|
716
|
+
else:
|
|
717
|
+
minecraft_dir = Path.home() / ".minecraft"
|
|
718
|
+
|
|
719
|
+
if not minecraft_dir.exists():
|
|
720
|
+
console.print(f"[red]Minecraft directory not found: {minecraft_dir}[/red]")
|
|
721
|
+
raise typer.Exit(1)
|
|
722
|
+
|
|
723
|
+
# Use pack name if profile name not specified
|
|
724
|
+
if not profile_name:
|
|
725
|
+
profile_name = pack_name
|
|
726
|
+
|
|
727
|
+
console.print(f"[cyan]Creating SKLauncher profile '{profile_name}'...[/cyan]")
|
|
728
|
+
|
|
729
|
+
# Create instance directory
|
|
730
|
+
instance_dir = minecraft_dir / "instances" / profile_name
|
|
731
|
+
instance_dir.mkdir(parents=True, exist_ok=True)
|
|
732
|
+
|
|
733
|
+
# Copy mods
|
|
734
|
+
dst_mods = instance_dir / "mods"
|
|
735
|
+
if dst_mods.exists():
|
|
736
|
+
shutil.rmtree(dst_mods)
|
|
737
|
+
shutil.copytree(mods_dir, dst_mods)
|
|
738
|
+
mod_count = len(list(dst_mods.glob("*.jar")))
|
|
739
|
+
console.print(f"[green]✓ Copied {mod_count} mods[/green]")
|
|
740
|
+
|
|
741
|
+
# Copy overrides
|
|
742
|
+
overrides_src = pack_path / "overrides"
|
|
743
|
+
if overrides_src.exists():
|
|
744
|
+
for item in overrides_src.iterdir():
|
|
745
|
+
dst = instance_dir / item.name
|
|
746
|
+
if item.is_dir():
|
|
747
|
+
if dst.exists():
|
|
748
|
+
shutil.rmtree(dst)
|
|
749
|
+
shutil.copytree(item, dst)
|
|
750
|
+
else:
|
|
751
|
+
shutil.copy2(item, dst)
|
|
752
|
+
console.print("[green]✓ Copied overrides[/green]")
|
|
753
|
+
|
|
754
|
+
# Update launcher_profiles.json
|
|
755
|
+
profiles_file = minecraft_dir / "launcher_profiles.json"
|
|
756
|
+
|
|
757
|
+
if profiles_file.exists():
|
|
758
|
+
profiles_data = json.loads(profiles_file.read_text())
|
|
759
|
+
else:
|
|
760
|
+
profiles_data = {"profiles": {}, "settings": {}, "version": 3}
|
|
761
|
+
|
|
762
|
+
# Create profile entry
|
|
763
|
+
from datetime import datetime
|
|
764
|
+
|
|
765
|
+
profile_id = profile_name.lower().replace(" ", "_").replace("-", "_")
|
|
766
|
+
loader_version = manifest.loader_version or FABRIC_LOADER_VERSION
|
|
767
|
+
|
|
768
|
+
profiles_data["profiles"][profile_id] = {
|
|
769
|
+
"name": profile_name,
|
|
770
|
+
"type": "custom",
|
|
771
|
+
"created": datetime.now().isoformat() + "Z",
|
|
772
|
+
"lastUsed": datetime.now().isoformat() + "Z",
|
|
773
|
+
"icon": "Furnace_On",
|
|
774
|
+
"lastVersionId": f"fabric-loader-{loader_version}-{manifest.minecraft}",
|
|
775
|
+
"gameDir": str(instance_dir),
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
# Save profiles
|
|
779
|
+
profiles_file.write_text(json.dumps(profiles_data, indent=2))
|
|
780
|
+
|
|
781
|
+
console.print("\n[green bold]✓ SKLauncher profile created![/green bold]")
|
|
782
|
+
console.print(f"\n[cyan]Profile:[/cyan] {profile_name}")
|
|
783
|
+
console.print(f"[cyan]Location:[/cyan] {instance_dir}")
|
|
784
|
+
console.print(f"[cyan]Version:[/cyan] fabric-loader-{loader_version}-{manifest.minecraft}")
|
|
785
|
+
console.print("\n[yellow]Next steps:[/yellow]")
|
|
786
|
+
console.print(" 1. Close SKLauncher if it's open")
|
|
787
|
+
console.print(" 2. Restart SKLauncher")
|
|
788
|
+
console.print(f" 3. Select profile '{profile_name}'")
|
|
789
|
+
console.print(" 4. If Fabric isn't installed, install it from SKLauncher:")
|
|
790
|
+
console.print(f" - MC: {manifest.minecraft}")
|
|
791
|
+
console.print(f" - Fabric: {loader_version}")
|
|
792
|
+
|
|
793
|
+
|
|
520
794
|
def main() -> None:
|
|
521
795
|
app()
|
|
522
796
|
|
|
@@ -68,7 +68,7 @@ class ModDownloader:
|
|
|
68
68
|
# Prioritize by version type: release > beta > alpha
|
|
69
69
|
version_priority = {"release": 3, "beta": 2, "alpha": 1}
|
|
70
70
|
|
|
71
|
-
def version_score(v):
|
|
71
|
+
def version_score(v) -> int:
|
|
72
72
|
vtype = v.get("version_type", "alpha")
|
|
73
73
|
return version_priority.get(vtype, 0)
|
|
74
74
|
|
|
@@ -155,13 +155,12 @@ class ModDownloader:
|
|
|
155
155
|
None,
|
|
156
156
|
)
|
|
157
157
|
|
|
158
|
-
if existing_entry:
|
|
158
|
+
if existing_entry and dest.exists():
|
|
159
159
|
# Verify hash matches
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return
|
|
160
|
+
existing_hash = hashlib.sha1(dest.read_bytes()).hexdigest()
|
|
161
|
+
if existing_hash == primary_file["hashes"]["sha1"]:
|
|
162
|
+
console.print(f"[dim]✓ {primary_file['filename']} (cached)[/dim]")
|
|
163
|
+
return
|
|
165
164
|
|
|
166
165
|
# Download the file
|
|
167
166
|
try:
|
|
@@ -193,7 +192,6 @@ class ModDownloader:
|
|
|
193
192
|
file_entry = {
|
|
194
193
|
"path": f"mods/{primary_file['filename']}",
|
|
195
194
|
"hashes": {"sha1": sha1, "sha512": sha512},
|
|
196
|
-
"env": {"client": "required", "server": "required"},
|
|
197
195
|
"downloads": [primary_file["url"]],
|
|
198
196
|
"fileSize": primary_file["size"],
|
|
199
197
|
}
|
|
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
|