modforge-cli 0.2.3__py3-none-any.whl

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.
@@ -0,0 +1,7 @@
1
+ """
2
+ CLI package for ModForge-CLI
3
+ """
4
+
5
+ from . import main
6
+
7
+ __all__ = ["main"]
@@ -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,5 @@
1
+ """
2
+ Auto-generated file. DO NOT EDIT.
3
+ """
4
+ __version__ = "0.2.3"
5
+ __author__ = "Frank1o3"
@@ -0,0 +1,7 @@
1
+ """
2
+ api package - Exposes the Modrinth API client globally.
3
+ """
4
+
5
+ from .modrinth import ModrinthAPIConfig
6
+
7
+ __all__ = ["ModrinthAPIConfig"]
@@ -0,0 +1,210 @@
1
+ """
2
+ api/modrith_api.py - Modrinth API v2 URL builder using modrinth_api.json
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+ from urllib.parse import quote_plus
11
+
12
+
13
+ class ModrinthAPIConfig:
14
+ """Loads modrinth_api.json and builds Modrinth API URLs."""
15
+
16
+ def __init__(self, config_path: str | Path = "configs/modrinth_api.json"):
17
+ self.config_path = config_path if isinstance(config_path, Path) else Path(config_path)
18
+ self.base_url: str = ""
19
+ self.endpoints: Dict[str, Any] = {}
20
+ self._load_config()
21
+
22
+ def _load_config(self) -> None:
23
+ if not self.config_path.exists():
24
+ raise FileNotFoundError(f"Modrinth API config not found: {self.config_path}")
25
+
26
+ with open(self.config_path, "r", encoding="utf-8") as f:
27
+ data = json.load(f)
28
+
29
+ self.base_url = data.get("BASE_URL", "").rstrip("/")
30
+ if not self.base_url:
31
+ raise ValueError("BASE_URL missing in modrinth_api.json")
32
+
33
+ self.endpoints = data.get("ENDPOINTS", {})
34
+ if not isinstance(self.endpoints, Dict):
35
+ raise ValueError("ENDPOINTS section is invalid")
36
+
37
+ def build_url(self, template: str, **kwargs: str) -> str:
38
+ """Format a template string with kwargs and prepend base URL."""
39
+ try:
40
+ path = template.format(**kwargs)
41
+ return f"{self.base_url}{path}"
42
+ except KeyError as e:
43
+ raise ValueError(f"Missing URL parameter: {e}")
44
+
45
+ # === Search ===
46
+
47
+ def search(
48
+ self,
49
+ query: Optional[str] = None,
50
+ facets: Optional[List[List[str]] | str] = None,
51
+ categories: Optional[List[str]] = None,
52
+ loaders: Optional[List[str]] = None,
53
+ game_versions: Optional[List[str]] = None,
54
+ license_: Optional[str] = None,
55
+ project_type: Optional[str] = None,
56
+ offset: Optional[int] = None,
57
+ limit: Optional[int] = 10,
58
+ index: Optional[str] = "relevance",
59
+ ) -> str:
60
+ """
61
+ Build the Modrinth search URL with query parameters.
62
+
63
+ Docs: https://docs.modrinth.com/api-spec#endpoints-search
64
+
65
+ Facets format: [[inner AND], [inner AND]] = outer OR
66
+ Example: [["categories:performance"], ["project_type:mod"]]
67
+
68
+ Args:
69
+ query: Search term (e.g., "sodium")
70
+ facets: Advanced filters as list of lists or JSON string
71
+ categories: Filter by categories (e.g., ["performance"])
72
+ loaders: Filter by loaders (e.g., ["fabric", "quilt"])
73
+ game_versions: Filter by Minecraft versions (e.g., ["1.21.1"])
74
+ license_: Filter by license (e.g., "MIT")
75
+ project_type: "mod", "resourcepack", "shader", "modpack", "datapack"
76
+ offset: Pagination offset
77
+ limit: Results per page (max 100)
78
+ index: Sort by "relevance", "downloads", "updated", "newest"
79
+
80
+ Returns:
81
+ Full search URL with query parameters
82
+ """
83
+ base = self.build_url(self.endpoints["search"])
84
+ params = []
85
+ if query:
86
+ params.append(f"query={quote_plus(query)}")
87
+
88
+ facets_array = []
89
+ if facets:
90
+ if isinstance(facets, str):
91
+ params.append(f"facets={quote_plus(facets)}")
92
+ else:
93
+ facets_array.extend(facets)
94
+
95
+ if project_type:
96
+ facets_array.append([f"project_type:{project_type}"])
97
+ if categories:
98
+ [facets_array.append([f"categories:{c}"]) for c in categories]
99
+ if loaders:
100
+ facets_array.append([f"categories:{l}" for l in loaders])
101
+ if game_versions:
102
+ facets_array.append([f"versions:{v}" for v in game_versions])
103
+ if license_:
104
+ facets_array.append([f"license:{license_}"])
105
+
106
+ if facets_array and not (isinstance(facets, str)):
107
+ params.append(f"facets={quote_plus(json.dumps(facets_array))}")
108
+
109
+ if offset is not None:
110
+ params.append(f"offset={offset}")
111
+ if limit is not None:
112
+ params.append(f"limit={min(limit, 100)}")
113
+ if index:
114
+ params.append(f"index={index}")
115
+
116
+ return f"{base}?{'&'.join(params)}" if params else base
117
+
118
+ # === Projects ===
119
+
120
+ def project(self, project_id: str) -> str:
121
+ return self.build_url(self.endpoints["projects"]["project"], id=project_id)
122
+
123
+ def project_versions(self, project_id: str) -> str:
124
+ return self.build_url(self.endpoints["projects"]["project_versions"], id=project_id)
125
+
126
+ def project_dependencies(self, project_id: str) -> str:
127
+ return self.build_url(self.endpoints["projects"]["dependencies"], id=project_id)
128
+
129
+ def project_gallery(self, project_id: str) -> str:
130
+ return self.build_url(self.endpoints["projects"]["gallery"], id=project_id)
131
+
132
+ def project_icon(self, project_id: str) -> str:
133
+ return self.build_url(self.endpoints["projects"]["icon"], id=project_id)
134
+
135
+ def check_following(self, project_id: str) -> str:
136
+ return self.build_url(self.endpoints["projects"]["check_following"], id=project_id)
137
+
138
+ # === Versions ===
139
+
140
+ def version(self, version_id: str) -> str:
141
+ return self.build_url(self.endpoints["versions"]["version"], id=version_id)
142
+
143
+ def version_files(self, version_id: str) -> str:
144
+ return self.build_url(self.endpoints["versions"]["files"], id=version_id)
145
+
146
+ def version_file_download(self, version_id: str, filename: str) -> str:
147
+ return self.build_url(
148
+ self.endpoints["versions"]["download"], id=version_id, filename=filename
149
+ )
150
+
151
+ def file_by_hash(self, hash_: str) -> str:
152
+ return self.build_url(self.endpoints["versions"]["file_by_hash"], hash=hash_)
153
+
154
+ def versions_by_hash(self, hash_: str) -> str:
155
+ return self.build_url(self.endpoints["versions"]["versions_by_hash"], hash=hash_)
156
+
157
+ def latest_version_for_hash(self, hash_: str, algorithm: str = "sha1") -> str:
158
+ return self.build_url(
159
+ self.endpoints["versions"]["latest_for_hash"], hash=hash_, algo=algorithm
160
+ )
161
+
162
+ # === Tags ===
163
+
164
+ def categories(self) -> str:
165
+ return self.build_url(self.endpoints["tags"]["categories"])
166
+
167
+ def loaders(self) -> str:
168
+ return self.build_url(self.endpoints["tags"]["loaders"])
169
+
170
+ def game_versions(self) -> str:
171
+ return self.build_url(self.endpoints["tags"]["game_versions"])
172
+
173
+ def licenses(self) -> str:
174
+ return self.build_url(self.endpoints["tags"]["licenses"])
175
+
176
+ def environments(self) -> str:
177
+ return self.build_url(self.endpoints["tags"]["environments"])
178
+
179
+ # === Teams ===
180
+
181
+ def team(self, team_id: str) -> str:
182
+ return self.build_url(self.endpoints["teams"]["team"], id=team_id)
183
+
184
+ def team_members(self, team_id: str) -> str:
185
+ return self.build_url(self.endpoints["teams"]["members"], id=team_id)
186
+
187
+ # === User ===
188
+
189
+ def user(self, user_id: str) -> str:
190
+ return self.build_url(self.endpoints["user"]["user"], id=user_id)
191
+
192
+ def user_projects(self, user_id: str) -> str:
193
+ return self.build_url(self.endpoints["user"]["user_projects"], id=user_id)
194
+
195
+ def user_notifications(self, user_id: str) -> str:
196
+ return self.build_url(self.endpoints["user"]["notifications"], id=user_id)
197
+
198
+ def user_avatar(self, user_id: str) -> str:
199
+ return self.build_url(self.endpoints["user"]["avatar"], id=user_id)
200
+
201
+ # === Bulk ===
202
+
203
+ def bulk_projects(self) -> str:
204
+ return self.build_url(self.endpoints["bulk"]["projects"])
205
+
206
+ def bulk_versions(self) -> str:
207
+ return self.build_url(self.endpoints["bulk"]["versions"])
208
+
209
+ def bulk_version_files(self) -> str:
210
+ return self.build_url(self.endpoints["bulk"]["version_files"])
@@ -0,0 +1,7 @@
1
+ """
2
+ Commands package - Exports all command groups
3
+ """
4
+
5
+ from . import export, modpack, project, setup, sklauncher, utils
6
+
7
+ __all__ = ["setup", "project", "modpack", "export", "sklauncher", "utils"]
@@ -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)