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.
Files changed (26) hide show
  1. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/PKG-INFO +1 -1
  2. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/pyproject.toml +1 -1
  3. modforge_cli-0.2.3/src/modforge_cli/__init__.py +7 -0
  4. modforge_cli-0.2.3/src/modforge_cli/__main__.py +132 -0
  5. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/__version__.py +1 -1
  6. modforge_cli-0.2.3/src/modforge_cli/cli/__init__.py +7 -0
  7. modforge_cli-0.2.3/src/modforge_cli/cli/export.py +246 -0
  8. modforge_cli-0.2.3/src/modforge_cli/cli/modpack.py +150 -0
  9. modforge_cli-0.2.3/src/modforge_cli/cli/project.py +72 -0
  10. modforge_cli-0.2.3/src/modforge_cli/cli/setup.py +72 -0
  11. modforge_cli-0.2.3/src/modforge_cli/cli/shared.py +41 -0
  12. modforge_cli-0.2.3/src/modforge_cli/cli/sklauncher.py +125 -0
  13. modforge_cli-0.2.3/src/modforge_cli/cli/utils.py +64 -0
  14. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/utils.py +159 -5
  15. modforge_cli-0.2.2/src/modforge_cli/__init__.py +0 -3
  16. modforge_cli-0.2.2/src/modforge_cli/__main__.py +0 -8
  17. modforge_cli-0.2.2/src/modforge_cli/cli.py +0 -799
  18. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/LICENSE +0 -0
  19. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/README.md +0 -0
  20. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/api/__init__.py +0 -0
  21. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/api/modrinth.py +0 -0
  22. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/__init__.py +0 -0
  23. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/downloader.py +0 -0
  24. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/models.py +0 -0
  25. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/policy.py +0 -0
  26. {modforge_cli-0.2.2 → modforge_cli-0.2.3}/src/modforge_cli/core/resolver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modforge-cli
3
- Version: 0.2.2
3
+ Version: 0.2.3
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.2.2"
3
+ version = "0.2.3"
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" }
@@ -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()
@@ -1,5 +1,5 @@
1
1
  """
2
2
  Auto-generated file. DO NOT EDIT.
3
3
  """
4
- __version__ = "0.2.2"
4
+ __version__ = "0.2.3"
5
5
  __author__ = "Frank1o3"
@@ -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)
@@ -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]")