Skip to content

Commit 8d5e6d8

Browse files
committed
Merge PR #4: Add get_project_version tool
2 parents 88a9ed7 + 0f5bc4c commit 8d5e6d8

12 files changed

Lines changed: 1515 additions & 36 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Publish Python 🐍 distribution 📦 to PyPI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- 'v*'
9+
10+
jobs:
11+
build-and-publish:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: '3.x'
19+
- name: Install build
20+
run: pip install build
21+
- name: Build package
22+
run: python -m build
23+
- name: Publish package to PyPI
24+
uses: pypa/gh-action-pypi-publish@release/v1
25+
with:
26+
password: ${{ secrets.PYPI_API_TOKEN }}

.vscode/tasks.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "Test minecode-mcp",
6+
"type": "shell",
7+
"command": "pytest",
8+
"group": "test"
9+
},
10+
{
11+
"label": "Test minecode-mcp",
12+
"type": "shell",
13+
"command": "C:/Users/antoi/Desktop/Informatique/2024-2025/HakatonEcole42/MineCode/venv/Scripts/python.exe -m pytest",
14+
"group": "test"
15+
},
16+
{
17+
"label": "Test minecode-mcp",
18+
"type": "shell",
19+
"command": "C:/Users/antoi/Desktop/Informatique/2024-2025/HakatonEcole42/MineCode/venv/Scripts/python.exe -m pytest",
20+
"group": "test"
21+
}
22+
]
23+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"folders": [
3+
{
4+
"path": "../../../../.."
5+
}
6+
],
7+
"settings": {}
8+
}

minecode/launchers/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
Minecraft Launcher Support Module
3+
Provides abstractions and implementations for various Minecraft launchers
4+
"""
5+
6+
from .base import BaseLauncher, LauncherInfo
7+
from .vanilla_launcher import VanillaLauncher
8+
from .multimc import MultiMCLauncher
9+
from .prism_launcher import PrismLauncherHandler
10+
from .curseforge import CurseForgeLauncher
11+
from .manager import LauncherManager
12+
13+
__all__ = [
14+
"BaseLauncher",
15+
"LauncherInfo",
16+
"VanillaLauncher",
17+
"MultiMCLauncher",
18+
"PrismLauncherHandler",
19+
"CurseForgeLauncher",
20+
"LauncherManager",
21+
]

minecode/launchers/base.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
Base launcher interface and data structures
3+
"""
4+
5+
from abc import ABC, abstractmethod
6+
from dataclasses import dataclass
7+
from pathlib import Path
8+
from typing import Optional, List, Dict, Any
9+
10+
11+
@dataclass
12+
class LauncherInfo:
13+
"""Information about a launcher instance"""
14+
name: str
15+
version: Optional[str]
16+
path: Path
17+
java_executable: Optional[Path]
18+
launcher_type: str
19+
20+
21+
class BaseLauncher(ABC):
22+
"""Abstract base class for Minecraft launcher implementations"""
23+
24+
def __init__(self, launcher_path: Path):
25+
"""
26+
Initialize launcher.
27+
28+
Args:
29+
launcher_path: Path to the launcher installation
30+
"""
31+
self.launcher_path = Path(launcher_path)
32+
33+
@abstractmethod
34+
def detect(self) -> bool:
35+
"""
36+
Check if this launcher is installed at the specified path.
37+
38+
Returns:
39+
True if launcher is detected, False otherwise
40+
"""
41+
pass
42+
43+
@abstractmethod
44+
def get_instances(self) -> List[Dict[str, Any]]:
45+
"""
46+
Get list of game instances/profiles.
47+
48+
Returns:
49+
List of instance dictionaries with name, path, version, etc.
50+
"""
51+
pass
52+
53+
@abstractmethod
54+
def get_logs(self, instance_name: str) -> Optional[str]:
55+
"""
56+
Get latest log content from an instance.
57+
58+
Args:
59+
instance_name: Name of the instance
60+
61+
Returns:
62+
Log content as string, or None if not found
63+
"""
64+
pass
65+
66+
@abstractmethod
67+
def clear_logs(self, instance_name: str) -> bool:
68+
"""
69+
Clear logs for an instance.
70+
71+
Args:
72+
instance_name: Name of the instance
73+
74+
Returns:
75+
True if successful, False otherwise
76+
"""
77+
pass
78+
79+
@abstractmethod
80+
def get_launcher_info(self) -> LauncherInfo:
81+
"""
82+
Get information about the launcher itself.
83+
84+
Returns:
85+
LauncherInfo object with launcher details
86+
"""
87+
pass
88+
89+
def get_instance_logs_directory(self, instance_name: str) -> Optional[Path]:
90+
"""
91+
Get the logs directory for an instance.
92+
93+
Args:
94+
instance_name: Name of the instance
95+
96+
Returns:
97+
Path to logs directory or None if not found
98+
"""
99+
pass

minecode/launchers/curseforge.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""
2+
CurseForge Minecraft launcher implementation
3+
"""
4+
5+
import json
6+
from pathlib import Path
7+
from typing import Optional, List, Dict, Any
8+
import platform
9+
10+
from .base import BaseLauncher, LauncherInfo
11+
12+
13+
class CurseForgeLauncher(BaseLauncher):
14+
"""Handler for CurseForge Minecraft launcher"""
15+
16+
LAUNCHER_NAME = "CurseForge Launcher"
17+
18+
def __init__(self, launcher_path: Optional[Path] = None):
19+
"""
20+
Initialize CurseForge Launcher handler.
21+
22+
Args:
23+
launcher_path: Path to CurseForge Launcher installation
24+
"""
25+
if launcher_path is None:
26+
launcher_path = self._get_default_path()
27+
28+
super().__init__(launcher_path)
29+
30+
@staticmethod
31+
def _get_default_path() -> Path:
32+
"""Get the default CurseForge Launcher directory path based on OS"""
33+
if platform.system() == "Windows":
34+
return Path.home() / "AppData" / "Local" / "CurseForge"
35+
elif platform.system() == "Darwin": # macOS
36+
return Path.home() / "Library" / "Application Support" / "CurseForge"
37+
else: # Linux
38+
return Path.home() / ".curseforge"
39+
40+
def detect(self) -> bool:
41+
"""Check if CurseForge Launcher is installed"""
42+
# Check for CurseForge application files
43+
return (self.launcher_path / "Instances").exists()
44+
45+
def get_launcher_info(self) -> LauncherInfo:
46+
"""Get information about CurseForge Launcher"""
47+
return LauncherInfo(
48+
name=self.LAUNCHER_NAME,
49+
version=None, # CurseForge doesn't expose version easily
50+
path=self.launcher_path,
51+
java_executable=self._find_java_executable(),
52+
launcher_type="curseforge"
53+
)
54+
55+
def get_instances(self) -> List[Dict[str, Any]]:
56+
"""Get list of CurseForge modpack instances"""
57+
instances = []
58+
instances_dir = self.launcher_path / "Instances"
59+
60+
if not instances_dir.exists():
61+
return instances
62+
63+
for instance_path in instances_dir.iterdir():
64+
if not instance_path.is_dir():
65+
continue
66+
67+
# Read manifest.json or instance info
68+
manifest_file = instance_path / "manifest.json"
69+
70+
instance_info = {
71+
"name": instance_path.name,
72+
"path": str(instance_path),
73+
"type": "modpack",
74+
"launcher": "curseforge"
75+
}
76+
77+
if manifest_file.exists():
78+
try:
79+
with open(manifest_file, 'r', encoding='utf-8') as f:
80+
manifest = json.load(f)
81+
instance_info["version"] = manifest.get("minecraft", {}).get("version", "Unknown")
82+
except (json.JSONDecodeError, IOError):
83+
instance_info["version"] = "Unknown"
84+
else:
85+
instance_info["version"] = "Unknown"
86+
87+
instances.append(instance_info)
88+
89+
return instances
90+
91+
def get_logs(self, instance_name: str) -> Optional[str]:
92+
"""Get latest log content from a CurseForge instance"""
93+
logs_dir = self.get_instance_logs_directory(instance_name)
94+
95+
if not logs_dir or not logs_dir.exists():
96+
return None
97+
98+
# CurseForge stores logs in .minecraft/logs
99+
latest_log = logs_dir / "latest.log"
100+
if latest_log.exists():
101+
try:
102+
with open(latest_log, 'r', encoding='utf-8', errors='ignore') as f:
103+
return f.read()
104+
except IOError as e:
105+
return f"Error reading log: {e}"
106+
107+
return None
108+
109+
def clear_logs(self, instance_name: str) -> bool:
110+
"""Clear logs for a CurseForge instance"""
111+
logs_dir = self.get_instance_logs_directory(instance_name)
112+
113+
if not logs_dir or not logs_dir.exists():
114+
return False
115+
116+
try:
117+
for log_file in logs_dir.glob("*.log"):
118+
log_file.unlink()
119+
return True
120+
except (IOError, OSError) as e:
121+
print(f"Error clearing logs: {e}")
122+
return False
123+
124+
def get_instance_logs_directory(self, instance_name: str) -> Optional[Path]:
125+
"""Get logs directory for a CurseForge instance"""
126+
instance_path = self.launcher_path / "Instances" / instance_name
127+
128+
if not instance_path.exists():
129+
return None
130+
131+
# CurseForge typically uses a minecraft directory structure
132+
logs_dir = instance_path / ".minecraft" / "logs"
133+
if not logs_dir.exists():
134+
# Some versions might have different structure
135+
logs_dir = instance_path / "logs"
136+
137+
return logs_dir if logs_dir.exists() else None
138+
139+
@staticmethod
140+
def _find_java_executable() -> Optional[Path]:
141+
"""Try to find Java executable"""
142+
import shutil
143+
java_path = shutil.which("java")
144+
return Path(java_path) if java_path else None

0 commit comments

Comments
 (0)