modforge-cli 0.2.2__tar.gz → 0.2.3__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.2 → modforge_cli-0.2.3}/PKG-INFO +1 -1
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/pyproject.toml +1 -1
- modforge_cli-0.2.3/src/modforge_cli/__init__.py +7 -0
- modforge_cli-0.2.3/src/modforge_cli/__main__.py +132 -0
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/__version__.py +1 -1
- modforge_cli-0.2.3/src/modforge_cli/cli/__init__.py +7 -0
- modforge_cli-0.2.3/src/modforge_cli/cli/export.py +246 -0
- modforge_cli-0.2.3/src/modforge_cli/cli/modpack.py +150 -0
- modforge_cli-0.2.3/src/modforge_cli/cli/project.py +72 -0
- modforge_cli-0.2.3/src/modforge_cli/cli/setup.py +72 -0
- modforge_cli-0.2.3/src/modforge_cli/cli/shared.py +41 -0
- modforge_cli-0.2.3/src/modforge_cli/cli/sklauncher.py +125 -0
- modforge_cli-0.2.3/src/modforge_cli/cli/utils.py +64 -0
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/utils.py +159 -5
- modforge_cli-0.2.2/src/modforge_cli/__init__.py +0 -3
- modforge_cli-0.2.2/src/modforge_cli/__main__.py +0 -8
- modforge_cli-0.2.2/src/modforge_cli/cli.py +0 -799
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/LICENSE +0 -0
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/README.md +0 -0
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/api/__init__.py +0 -0
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/api/modrinth.py +0 -0
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/__init__.py +0 -0
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/downloader.py +0 -0
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/models.py +0 -0
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/policy.py +0 -0
- {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/resolver.py +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main CLI entry point - Registers all commands
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from pyfiglet import figlet_format
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from modforge_cli.cli import export, modpack, project, setup, sklauncher, utils
|
|
13
|
+
from modforge_cli.cli.shared import (
|
|
14
|
+
DEFAULT_MODRINTH_API_URL,
|
|
15
|
+
DEFAULT_POLICY_URL,
|
|
16
|
+
MODRINTH_API,
|
|
17
|
+
POLICY_PATH,
|
|
18
|
+
console,
|
|
19
|
+
get_version_info,
|
|
20
|
+
)
|
|
21
|
+
from modforge_cli.core import ensure_config_file, setup_crash_logging
|
|
22
|
+
|
|
23
|
+
# Get version info
|
|
24
|
+
__version__, __author__ = get_version_info()
|
|
25
|
+
|
|
26
|
+
# Create main app
|
|
27
|
+
app = typer.Typer(
|
|
28
|
+
add_completion=False,
|
|
29
|
+
no_args_is_help=False,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Setup crash logging
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
LOG_DIR = setup_crash_logging()
|
|
36
|
+
|
|
37
|
+
# Ensure configs exist
|
|
38
|
+
ensure_config_file(MODRINTH_API, DEFAULT_MODRINTH_API_URL, "Modrinth API", console)
|
|
39
|
+
ensure_config_file(POLICY_PATH, DEFAULT_POLICY_URL, "Policy", console)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def render_banner() -> None:
|
|
43
|
+
"""Renders a stylized banner"""
|
|
44
|
+
width = console.width
|
|
45
|
+
font = "slant" if width > 60 else "small"
|
|
46
|
+
|
|
47
|
+
ascii_art = figlet_format("ModForge-CLI", font=font)
|
|
48
|
+
banner_text = Text(ascii_art, style="bold cyan")
|
|
49
|
+
|
|
50
|
+
info_line = Text.assemble(
|
|
51
|
+
(" ⛏ ", "yellow"),
|
|
52
|
+
(f"v{__version__}", "bold white"),
|
|
53
|
+
(" | ", "dim"),
|
|
54
|
+
("Created by ", "italic white"),
|
|
55
|
+
(f"{__author__}", "bold magenta"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
console.print(
|
|
59
|
+
Panel(
|
|
60
|
+
Text.assemble(banner_text, "\n", info_line),
|
|
61
|
+
border_style="blue",
|
|
62
|
+
padding=(1, 2),
|
|
63
|
+
expand=False,
|
|
64
|
+
),
|
|
65
|
+
justify="left",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.callback(invoke_without_command=True)
|
|
70
|
+
def main_callback(
|
|
71
|
+
ctx: typer.Context,
|
|
72
|
+
version: bool | None = typer.Option(None, "--version", "-v", help="Show version and exit"),
|
|
73
|
+
verbose: bool | None = typer.Option(None, "--verbose", help="Enable verbose logging"),
|
|
74
|
+
) -> None:
|
|
75
|
+
"""ModForge-CLI: A powerful Minecraft modpack manager for Modrinth."""
|
|
76
|
+
|
|
77
|
+
if verbose:
|
|
78
|
+
# Enable verbose logging
|
|
79
|
+
logging.basicConfig(
|
|
80
|
+
level=logging.DEBUG,
|
|
81
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
82
|
+
handlers=[
|
|
83
|
+
logging.FileHandler(LOG_DIR / f"modforge-{__version__}.log"),
|
|
84
|
+
logging.StreamHandler(),
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if version:
|
|
89
|
+
console.print(f"ModForge-CLI Version: [bold cyan]{__version__}[/bold cyan]")
|
|
90
|
+
raise typer.Exit()
|
|
91
|
+
|
|
92
|
+
if ctx.invoked_subcommand is None:
|
|
93
|
+
render_banner()
|
|
94
|
+
console.print("\n[bold yellow]Usage:[/bold yellow] ModForge-CLI [COMMAND] [ARGS]...")
|
|
95
|
+
console.print("\n[bold cyan]Core Commands:[/bold cyan]")
|
|
96
|
+
console.print(" [green]setup[/green] Initialize a new modpack project")
|
|
97
|
+
console.print(" [green]ls[/green] List all registered projects")
|
|
98
|
+
console.print(" [green]add[/green] Add a mod/resource/shader to manifest")
|
|
99
|
+
console.print(" [green]resolve[/green] Resolve all dependencies")
|
|
100
|
+
console.print(" [green]build[/green] Download files and setup loader")
|
|
101
|
+
console.print(" [green]export[/green] Create the final .mrpack")
|
|
102
|
+
console.print(" [green]validate[/green] Check .mrpack for issues")
|
|
103
|
+
console.print(" [green]sklauncher[/green] Create SKLauncher profile (no .mrpack)")
|
|
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
|
+
console.print("\nRun [white]ModForge-CLI --help[/white] for details.\n")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Register all command groups
|
|
112
|
+
app.command()(setup.setup)
|
|
113
|
+
app.add_typer(project.app, name="project", help="Project management commands")
|
|
114
|
+
app.command("ls")(project.list_projects)
|
|
115
|
+
app.command()(project.remove)
|
|
116
|
+
app.command()(modpack.add)
|
|
117
|
+
app.command()(modpack.resolve)
|
|
118
|
+
app.command()(modpack.build)
|
|
119
|
+
app.command()(export.export)
|
|
120
|
+
app.command()(export.validate)
|
|
121
|
+
app.command()(sklauncher.sklauncher)
|
|
122
|
+
app.command()(utils.doctor)
|
|
123
|
+
app.command("self-update")(utils.self_update_cmd)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main() -> None:
|
|
127
|
+
"""Main entry point"""
|
|
128
|
+
app()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Export and validation commands
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
import zipfile
|
|
10
|
+
from zipfile import ZIP_DEFLATED, ZipFile
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from modforge_cli.cli.shared import REGISTRY_PATH, console
|
|
15
|
+
from modforge_cli.core import get_manifest, load_registry
|
|
16
|
+
|
|
17
|
+
app = typer.Typer()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def export(pack_name: str | None = None) -> None:
|
|
22
|
+
"""Create final .mrpack file"""
|
|
23
|
+
|
|
24
|
+
if not pack_name:
|
|
25
|
+
manifest = get_manifest(console, Path.cwd())
|
|
26
|
+
if manifest:
|
|
27
|
+
pack_name = manifest.name
|
|
28
|
+
else:
|
|
29
|
+
console.print("[red]No manifest found[/red]")
|
|
30
|
+
raise typer.Exit(1)
|
|
31
|
+
|
|
32
|
+
registry = load_registry(REGISTRY_PATH)
|
|
33
|
+
if pack_name not in registry:
|
|
34
|
+
console.print(f"[red]Pack '{pack_name}' not found[/red]")
|
|
35
|
+
raise typer.Exit(1)
|
|
36
|
+
|
|
37
|
+
pack_path = Path(registry[pack_name])
|
|
38
|
+
manifest = get_manifest(console, pack_path)
|
|
39
|
+
if not manifest:
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
|
|
42
|
+
console.print("[cyan]Exporting modpack...[/cyan]")
|
|
43
|
+
|
|
44
|
+
mods_dir = pack_path / "mods"
|
|
45
|
+
index_file = pack_path / "modrinth.index.json"
|
|
46
|
+
|
|
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
|
+
if not index_file.exists():
|
|
52
|
+
console.print("[red]No modrinth.index.json found[/red]")
|
|
53
|
+
raise typer.Exit(1)
|
|
54
|
+
|
|
55
|
+
# Validate index has files
|
|
56
|
+
index_data = json.loads(index_file.read_text())
|
|
57
|
+
if not index_data.get("files"):
|
|
58
|
+
console.print("[yellow]Warning: No files registered in index[/yellow]")
|
|
59
|
+
console.print("[yellow]This might cause issues. Run 'ModForge-CLI build' again.[/yellow]")
|
|
60
|
+
|
|
61
|
+
# Create .mrpack
|
|
62
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
63
|
+
tmp_path = Path(tmpdir)
|
|
64
|
+
|
|
65
|
+
# Copy modrinth.index.json to root
|
|
66
|
+
shutil.copy2(index_file, tmp_path / "modrinth.index.json")
|
|
67
|
+
|
|
68
|
+
# Copy overrides if they exist
|
|
69
|
+
overrides_src = pack_path / "overrides"
|
|
70
|
+
if overrides_src.exists():
|
|
71
|
+
overrides_dst = tmp_path / "overrides"
|
|
72
|
+
shutil.copytree(overrides_src, overrides_dst)
|
|
73
|
+
console.print("[green]✓ Copied overrides[/green]")
|
|
74
|
+
|
|
75
|
+
# Create .mrpack
|
|
76
|
+
mrpack_path = pack_path.parent / f"{pack_name}.mrpack"
|
|
77
|
+
|
|
78
|
+
with ZipFile(mrpack_path, "w", ZIP_DEFLATED) as zipf:
|
|
79
|
+
# Add modrinth.index.json at root
|
|
80
|
+
zipf.write(tmp_path / "modrinth.index.json", "modrinth.index.json")
|
|
81
|
+
|
|
82
|
+
# Add overrides folder if exists
|
|
83
|
+
if overrides_src.exists():
|
|
84
|
+
for file_path in (tmp_path / "overrides").rglob("*"):
|
|
85
|
+
if file_path.is_file():
|
|
86
|
+
arcname = str(file_path.relative_to(tmp_path))
|
|
87
|
+
zipf.write(file_path, arcname)
|
|
88
|
+
|
|
89
|
+
console.print(f"[green bold]✓ Exported to {mrpack_path}[/green bold]")
|
|
90
|
+
|
|
91
|
+
# Show summary
|
|
92
|
+
file_count = len(index_data.get("files", []))
|
|
93
|
+
console.print("\n[cyan]Summary:[/cyan]")
|
|
94
|
+
console.print(f" Files registered: {file_count}")
|
|
95
|
+
console.print(f" Minecraft: {index_data['dependencies'].get('minecraft')}")
|
|
96
|
+
|
|
97
|
+
# Show loader
|
|
98
|
+
for loader in ["fabric-loader", "quilt-loader", "forge", "neoforge"]:
|
|
99
|
+
if loader in index_data["dependencies"]:
|
|
100
|
+
console.print(f" Loader: {loader} {index_data['dependencies'][loader]}")
|
|
101
|
+
|
|
102
|
+
console.print("\n[dim]Import this in SKLauncher, Prism, ATLauncher, etc.[/dim]")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command()
|
|
106
|
+
def validate(mrpack_file: str | None = None) -> None:
|
|
107
|
+
"""Validate .mrpack file for launcher compatibility"""
|
|
108
|
+
|
|
109
|
+
if not mrpack_file:
|
|
110
|
+
# Look for .mrpack in current directory
|
|
111
|
+
mrpacks = list(Path.cwd().glob("*.mrpack"))
|
|
112
|
+
if not mrpacks:
|
|
113
|
+
console.print("[red]No .mrpack file found in current directory[/red]")
|
|
114
|
+
console.print("[yellow]Usage: ModForge-CLI validate <file.mrpack>[/yellow]")
|
|
115
|
+
raise typer.Exit(1)
|
|
116
|
+
mrpack_path = mrpacks[0]
|
|
117
|
+
else:
|
|
118
|
+
mrpack_path = Path(mrpack_file)
|
|
119
|
+
|
|
120
|
+
if not mrpack_path.exists():
|
|
121
|
+
console.print(f"[red]File not found: {mrpack_path}[/red]")
|
|
122
|
+
raise typer.Exit(1)
|
|
123
|
+
|
|
124
|
+
console.print(f"[cyan]Validating {mrpack_path.name}...[/cyan]\n")
|
|
125
|
+
|
|
126
|
+
issues = []
|
|
127
|
+
warnings = []
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
with zipfile.ZipFile(mrpack_path, "r") as z:
|
|
131
|
+
files = z.namelist()
|
|
132
|
+
|
|
133
|
+
# Check for modrinth.index.json
|
|
134
|
+
if "modrinth.index.json" not in files:
|
|
135
|
+
console.print("[red]❌ CRITICAL: modrinth.index.json not found at root[/red]")
|
|
136
|
+
raise typer.Exit(1)
|
|
137
|
+
|
|
138
|
+
console.print("[green]✅ modrinth.index.json found[/green]")
|
|
139
|
+
|
|
140
|
+
# Read and validate index
|
|
141
|
+
index_data = json.loads(z.read("modrinth.index.json"))
|
|
142
|
+
|
|
143
|
+
# Check required fields
|
|
144
|
+
required = ["formatVersion", "game", "versionId", "name", "dependencies"]
|
|
145
|
+
for field in required:
|
|
146
|
+
if field not in index_data:
|
|
147
|
+
issues.append(f"Missing required field: {field}")
|
|
148
|
+
console.print(f"[red]❌ Missing: {field}[/red]")
|
|
149
|
+
else:
|
|
150
|
+
value = index_data[field]
|
|
151
|
+
if isinstance(value, dict):
|
|
152
|
+
console.print(f"[green]✅ {field}[/green]")
|
|
153
|
+
else:
|
|
154
|
+
console.print(f"[green]✅ {field}: {value}[/green]")
|
|
155
|
+
|
|
156
|
+
# Check dependencies
|
|
157
|
+
deps = index_data.get("dependencies", {})
|
|
158
|
+
if "minecraft" not in deps:
|
|
159
|
+
issues.append("Missing minecraft in dependencies")
|
|
160
|
+
console.print("[red]❌ Missing: minecraft version[/red]")
|
|
161
|
+
else:
|
|
162
|
+
console.print(f"[green]✅ Minecraft: {deps['minecraft']}[/green]")
|
|
163
|
+
|
|
164
|
+
# Check for loader
|
|
165
|
+
loaders = ["fabric-loader", "quilt-loader", "forge", "neoforge"]
|
|
166
|
+
has_loader = any(l in deps for l in loaders)
|
|
167
|
+
|
|
168
|
+
if not has_loader:
|
|
169
|
+
issues.append("No mod loader in dependencies")
|
|
170
|
+
console.print("[red]❌ Missing mod loader[/red]")
|
|
171
|
+
else:
|
|
172
|
+
for loader in loaders:
|
|
173
|
+
if loader in deps:
|
|
174
|
+
console.print(f"[green]✅ Loader: {loader} = {deps[loader]}[/green]")
|
|
175
|
+
|
|
176
|
+
# Check files array
|
|
177
|
+
files_list = index_data.get("files", [])
|
|
178
|
+
console.print(f"\n[cyan]📦 Files registered: {len(files_list)}[/cyan]")
|
|
179
|
+
|
|
180
|
+
if len(files_list) == 0:
|
|
181
|
+
warnings.append("No files in array (pack might not work)")
|
|
182
|
+
console.print("[yellow]⚠️ WARNING: files array is empty[/yellow]")
|
|
183
|
+
else:
|
|
184
|
+
# Check first file structure
|
|
185
|
+
sample = files_list[0]
|
|
186
|
+
file_required = ["path", "hashes", "downloads", "fileSize"]
|
|
187
|
+
|
|
188
|
+
missing_fields = [f for f in file_required if f not in sample]
|
|
189
|
+
if missing_fields:
|
|
190
|
+
issues.append(f"Files missing fields: {missing_fields}")
|
|
191
|
+
console.print(f"[red]❌ Files missing: {', '.join(missing_fields)}[/red]")
|
|
192
|
+
else:
|
|
193
|
+
console.print("[green]✅ File structure looks good[/green]")
|
|
194
|
+
|
|
195
|
+
# Check hashes
|
|
196
|
+
if "hashes" in sample:
|
|
197
|
+
if "sha1" not in sample["hashes"]:
|
|
198
|
+
issues.append("Files missing sha1 hash")
|
|
199
|
+
console.print("[red]❌ Missing sha1 hashes[/red]")
|
|
200
|
+
else:
|
|
201
|
+
console.print("[green]✅ sha1 hashes present[/green]")
|
|
202
|
+
|
|
203
|
+
if "sha512" not in sample["hashes"]:
|
|
204
|
+
warnings.append("Files missing sha512 hash")
|
|
205
|
+
console.print("[yellow]⚠️ Missing sha512 hashes (optional)[/yellow]")
|
|
206
|
+
else:
|
|
207
|
+
console.print("[green]✅ sha512 hashes present[/green]")
|
|
208
|
+
|
|
209
|
+
# Check env field
|
|
210
|
+
if "env" not in sample:
|
|
211
|
+
warnings.append("Files missing env field")
|
|
212
|
+
console.print("[yellow]⚠️ Missing env field (recommended)[/yellow]")
|
|
213
|
+
else:
|
|
214
|
+
console.print("[green]✅ env field present[/green]")
|
|
215
|
+
|
|
216
|
+
# Summary
|
|
217
|
+
console.print("\n" + "=" * 60)
|
|
218
|
+
|
|
219
|
+
if issues:
|
|
220
|
+
console.print(f"\n[red bold]❌ CRITICAL ISSUES ({len(issues)}):[/red bold]")
|
|
221
|
+
for issue in issues:
|
|
222
|
+
console.print(f" [red]• {issue}[/red]")
|
|
223
|
+
|
|
224
|
+
if warnings:
|
|
225
|
+
console.print(f"\n[yellow bold]⚠️ WARNINGS ({len(warnings)}):[/yellow bold]")
|
|
226
|
+
for warning in warnings:
|
|
227
|
+
console.print(f" [yellow]• {warning}[/yellow]")
|
|
228
|
+
|
|
229
|
+
if not issues and not warnings:
|
|
230
|
+
console.print("\n[green bold]✅ All checks passed![/green bold]")
|
|
231
|
+
console.print("[dim]Pack should work in all Modrinth-compatible launchers[/dim]")
|
|
232
|
+
elif not issues:
|
|
233
|
+
console.print("\n[green]✅ No critical issues[/green]")
|
|
234
|
+
console.print("[dim]Pack should work, but consider addressing warnings[/dim]")
|
|
235
|
+
else:
|
|
236
|
+
console.print("\n[red bold]❌ Pack has critical issues[/red bold]")
|
|
237
|
+
console.print("[yellow]Run 'ModForge-CLI build' again to fix[/yellow]")
|
|
238
|
+
raise typer.Exit(1)
|
|
239
|
+
|
|
240
|
+
except zipfile.BadZipFile:
|
|
241
|
+
console.print("[red]❌ ERROR: Not a valid ZIP/MRPACK file[/red]")
|
|
242
|
+
raise typer.Exit(1)
|
|
243
|
+
except json.JSONDecodeError as e:
|
|
244
|
+
console.print("[red]❌ ERROR: Invalid JSON in modrinth.index.json[/red]")
|
|
245
|
+
console.print(f"[dim]{e}[/dim]")
|
|
246
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modpack operations - Add, resolve, build
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from modforge_cli.api import ModrinthAPIConfig
|
|
11
|
+
from modforge_cli.cli.shared import MODRINTH_API, POLICY_PATH, REGISTRY_PATH, console
|
|
12
|
+
from modforge_cli.core import (
|
|
13
|
+
ModPolicy,
|
|
14
|
+
ModResolver,
|
|
15
|
+
get_api_session,
|
|
16
|
+
get_manifest,
|
|
17
|
+
load_registry,
|
|
18
|
+
perform_add,
|
|
19
|
+
run,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
app = typer.Typer()
|
|
23
|
+
|
|
24
|
+
# Initialize API
|
|
25
|
+
api = ModrinthAPIConfig(MODRINTH_API)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command()
|
|
29
|
+
def add(name: str, project_type: str = "mod", pack_name: str | None = None) -> None:
|
|
30
|
+
"""Add a project to the manifest"""
|
|
31
|
+
|
|
32
|
+
if project_type not in ["mod", "resourcepack", "shaderpack"]:
|
|
33
|
+
console.print(f"[red]Invalid type:[/red] {project_type}")
|
|
34
|
+
console.print("[yellow]Valid types:[/yellow] mod, resourcepack, shaderpack")
|
|
35
|
+
raise typer.Exit(1)
|
|
36
|
+
|
|
37
|
+
# Auto-detect pack if not specified
|
|
38
|
+
if not pack_name:
|
|
39
|
+
manifest = get_manifest(console, Path.cwd())
|
|
40
|
+
if manifest:
|
|
41
|
+
pack_name = manifest.name
|
|
42
|
+
else:
|
|
43
|
+
console.print("[red]No manifest found in current directory[/red]")
|
|
44
|
+
console.print("[yellow]Specify --pack-name or run from project directory[/yellow]")
|
|
45
|
+
raise typer.Exit(1)
|
|
46
|
+
|
|
47
|
+
registry = load_registry(REGISTRY_PATH)
|
|
48
|
+
if pack_name not in registry:
|
|
49
|
+
console.print(f"[red]Pack '{pack_name}' not found in registry[/red]")
|
|
50
|
+
console.print("[yellow]Available packs:[/yellow]")
|
|
51
|
+
for p in registry:
|
|
52
|
+
console.print(f" - {p}")
|
|
53
|
+
raise typer.Exit(1)
|
|
54
|
+
|
|
55
|
+
pack_path = Path(registry[pack_name])
|
|
56
|
+
manifest_file = pack_path / "ModForge-CLI.json"
|
|
57
|
+
|
|
58
|
+
manifest = get_manifest(console, pack_path)
|
|
59
|
+
if not manifest:
|
|
60
|
+
console.print(f"[red]Could not load manifest at {manifest_file}[/red]")
|
|
61
|
+
raise typer.Exit(1)
|
|
62
|
+
|
|
63
|
+
asyncio.run(perform_add(api, name, manifest, project_type, console, manifest_file))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command()
|
|
67
|
+
def resolve(pack_name: str | None = None) -> None:
|
|
68
|
+
"""Resolve all mod dependencies"""
|
|
69
|
+
|
|
70
|
+
# Auto-detect pack
|
|
71
|
+
if not pack_name:
|
|
72
|
+
manifest = get_manifest(console, Path.cwd())
|
|
73
|
+
if manifest:
|
|
74
|
+
pack_name = manifest.name
|
|
75
|
+
else:
|
|
76
|
+
console.print("[red]No manifest found[/red]")
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
|
|
79
|
+
registry = load_registry(REGISTRY_PATH)
|
|
80
|
+
if pack_name not in registry:
|
|
81
|
+
console.print(f"[red]Pack '{pack_name}' not found[/red]")
|
|
82
|
+
raise typer.Exit(1)
|
|
83
|
+
|
|
84
|
+
pack_path = Path(registry[pack_name])
|
|
85
|
+
manifest_file = pack_path / "ModForge-CLI.json"
|
|
86
|
+
|
|
87
|
+
manifest = get_manifest(console, pack_path)
|
|
88
|
+
if not manifest:
|
|
89
|
+
console.print("[red]Could not load manifest[/red]")
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
|
|
92
|
+
console.print(f"[cyan]Resolving dependencies for {pack_name}...[/cyan]")
|
|
93
|
+
|
|
94
|
+
policy = ModPolicy(POLICY_PATH)
|
|
95
|
+
resolver = ModResolver(
|
|
96
|
+
policy=policy, api=api, mc_version=manifest.minecraft, loader=manifest.loader
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def do_resolve():
|
|
100
|
+
async with await get_api_session() as session:
|
|
101
|
+
return await resolver.resolve(manifest.mods, session)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
resolved_mods = asyncio.run(do_resolve())
|
|
105
|
+
except Exception as e:
|
|
106
|
+
console.print(f"[red]Resolution failed:[/red] {e}")
|
|
107
|
+
raise typer.Exit(1) from e
|
|
108
|
+
|
|
109
|
+
manifest.mods = sorted(list(resolved_mods))
|
|
110
|
+
manifest_file.write_text(manifest.model_dump_json(indent=4))
|
|
111
|
+
|
|
112
|
+
console.print(f"[green]✓ Resolved {len(manifest.mods)} mods[/green]")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command()
|
|
116
|
+
def build(pack_name: str | None = None) -> None:
|
|
117
|
+
"""Download all mods and dependencies"""
|
|
118
|
+
|
|
119
|
+
if not pack_name:
|
|
120
|
+
manifest = get_manifest(console, Path.cwd())
|
|
121
|
+
if manifest:
|
|
122
|
+
pack_name = manifest.name
|
|
123
|
+
else:
|
|
124
|
+
console.print("[red]No manifest found[/red]")
|
|
125
|
+
raise typer.Exit(1)
|
|
126
|
+
|
|
127
|
+
registry = load_registry(REGISTRY_PATH)
|
|
128
|
+
if pack_name not in registry:
|
|
129
|
+
console.print(f"[red]Pack '{pack_name}' not found[/red]")
|
|
130
|
+
raise typer.Exit(1)
|
|
131
|
+
|
|
132
|
+
pack_path = Path(registry[pack_name])
|
|
133
|
+
manifest = get_manifest(console, pack_path)
|
|
134
|
+
if not manifest:
|
|
135
|
+
raise typer.Exit(1)
|
|
136
|
+
|
|
137
|
+
pack_root = pack_path
|
|
138
|
+
mods_dir = pack_root / "mods"
|
|
139
|
+
index_file = pack_root / "modrinth.index.json"
|
|
140
|
+
|
|
141
|
+
mods_dir.mkdir(exist_ok=True)
|
|
142
|
+
|
|
143
|
+
console.print(f"[cyan]Building {manifest.name}...[/cyan]")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
asyncio.run(run(api, manifest, mods_dir, index_file))
|
|
147
|
+
console.print("[green]✓ Build complete[/green]")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
console.print(f"[red]Build failed:[/red] {e}")
|
|
150
|
+
raise typer.Exit(1) from e
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project management commands - List and remove projects
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.prompt import Confirm
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from modforge_cli.cli.shared import REGISTRY_PATH, console
|
|
14
|
+
from modforge_cli.core import load_registry, save_registry_atomic
|
|
15
|
+
|
|
16
|
+
app = typer.Typer()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command(name="ls")
|
|
20
|
+
def list_projects() -> None:
|
|
21
|
+
"""List all registered modpacks"""
|
|
22
|
+
registry = load_registry(REGISTRY_PATH)
|
|
23
|
+
|
|
24
|
+
if not registry:
|
|
25
|
+
console.print("[yellow]No projects registered yet[/yellow]")
|
|
26
|
+
console.print("[dim]Run 'ModForge-CLI setup <name>' to create one[/dim]")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
table = Table(title="ModForge-CLI Projects", header_style="bold magenta")
|
|
30
|
+
table.add_column("Name", style="cyan")
|
|
31
|
+
table.add_column("Location", style="dim")
|
|
32
|
+
|
|
33
|
+
for name, path in registry.items():
|
|
34
|
+
table.add_row(name, path)
|
|
35
|
+
|
|
36
|
+
console.print(table)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command()
|
|
40
|
+
def remove(pack_name: str) -> None:
|
|
41
|
+
"""Remove a modpack and unregister it"""
|
|
42
|
+
registry = load_registry(REGISTRY_PATH)
|
|
43
|
+
|
|
44
|
+
if pack_name not in registry:
|
|
45
|
+
console.print(f"[red]Pack '{pack_name}' not found[/red]")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
|
|
48
|
+
pack_path = Path(registry[pack_name])
|
|
49
|
+
|
|
50
|
+
console.print(
|
|
51
|
+
Panel.fit(
|
|
52
|
+
f"[bold red]This will permanently delete:[/bold red]\n\n"
|
|
53
|
+
f"[white]{pack_name}[/white]\n"
|
|
54
|
+
f"[dim]{pack_path}[/dim]",
|
|
55
|
+
title="⚠️ Destructive Action",
|
|
56
|
+
border_style="red",
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not Confirm.ask("Are you sure?", default=False):
|
|
61
|
+
console.print("Aborted.")
|
|
62
|
+
raise typer.Exit()
|
|
63
|
+
|
|
64
|
+
# Remove directory
|
|
65
|
+
if pack_path.exists():
|
|
66
|
+
shutil.rmtree(pack_path)
|
|
67
|
+
|
|
68
|
+
# Update registry
|
|
69
|
+
del registry[pack_name]
|
|
70
|
+
save_registry_atomic(registry, REGISTRY_PATH)
|
|
71
|
+
|
|
72
|
+
console.print(f"[green]✓ Removed {pack_name}[/green]")
|