modforge-cli 0.1.5__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.
- modforge_cli/__init__.py +5 -0
- modforge_cli/__main__.py +8 -0
- modforge_cli/__version__.py +5 -0
- modforge_cli/api/__init__.py +7 -0
- modforge_cli/api/modrinth.py +218 -0
- modforge_cli/cli.py +559 -0
- modforge_cli/core/__init__.py +6 -0
- modforge_cli/core/downloader.py +89 -0
- modforge_cli/core/models.py +72 -0
- modforge_cli/core/policy.py +162 -0
- modforge_cli/core/resolver.py +134 -0
- modforge_cli-0.1.5.dist-info/METADATA +70 -0
- modforge_cli-0.1.5.dist-info/RECORD +16 -0
- modforge_cli-0.1.5.dist-info/WHEEL +4 -0
- modforge_cli-0.1.5.dist-info/entry_points.txt +3 -0
- modforge_cli-0.1.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import List, Dict, Literal
|
|
2
|
+
from pydantic import BaseModel, Field, TypeAdapter
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseAPIModel(BaseModel):
|
|
6
|
+
model_config = {"extra": "ignore"}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Manifest(BaseModel):
|
|
10
|
+
name: str
|
|
11
|
+
minecraft: str
|
|
12
|
+
loader: str
|
|
13
|
+
loader_version: str | None = None
|
|
14
|
+
mods: List[str] = Field(default_factory=list)
|
|
15
|
+
resourcepacks: List[str] = Field(default_factory=list)
|
|
16
|
+
shaderpacks: List[str] = Field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Hit(BaseAPIModel):
|
|
20
|
+
project_id: str
|
|
21
|
+
project_type: str
|
|
22
|
+
slug: str
|
|
23
|
+
categories: List[str] = Field(default_factory=list)
|
|
24
|
+
versions: List[str] = Field(default_factory=list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SearchResult(BaseAPIModel):
|
|
28
|
+
hits: List[Hit] = Field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Dependency(BaseAPIModel):
|
|
32
|
+
dependency_type: (
|
|
33
|
+
Literal["required", "optional", "incompatible", "embedded"] | None
|
|
34
|
+
) = None
|
|
35
|
+
file_name: str | None = None
|
|
36
|
+
project_id: str | None = None
|
|
37
|
+
version_id: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class File(BaseAPIModel):
|
|
41
|
+
id: str | None = None
|
|
42
|
+
hashes: Dict[str, str] = Field(default_factory=dict)
|
|
43
|
+
url: str | None = None
|
|
44
|
+
filename: str | None = None
|
|
45
|
+
primary: bool | None = None
|
|
46
|
+
size: int | None = None
|
|
47
|
+
file_type: str | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ProjectVersion(BaseAPIModel):
|
|
51
|
+
id: str
|
|
52
|
+
project_id: str
|
|
53
|
+
version_number: str
|
|
54
|
+
version_type: str
|
|
55
|
+
dependencies: List[Dependency] = Field(default_factory=list)
|
|
56
|
+
files: List[File] = Field(default_factory=list)
|
|
57
|
+
game_versions: List[str] = Field(default_factory=list)
|
|
58
|
+
loaders: List[str] = Field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_release(self) -> bool:
|
|
62
|
+
return self.version_type == "release"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
ProjectVersionList = TypeAdapter(List[ProjectVersion])
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"Manifest",
|
|
69
|
+
"SearchResult",
|
|
70
|
+
"ProjectVersion",
|
|
71
|
+
"ProjectVersionList",
|
|
72
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Iterable, List, Set, TypedDict
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
from urllib.request import urlopen
|
|
8
|
+
from collections import deque
|
|
9
|
+
|
|
10
|
+
from jsonschema import ValidationError, validate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NormalizedModRule(TypedDict):
|
|
14
|
+
conflicts: Set[str]
|
|
15
|
+
sub_mods: Set[str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
NormalizedPolicyRules = Dict[str, NormalizedModRule]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PolicyError(RuntimeError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# -------- schema cache (performance + offline safety) --------
|
|
26
|
+
_SCHEMA_CACHE: Dict[str, dict] = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_schema(schema_ref: str, base_path: Path) -> dict:
|
|
30
|
+
if schema_ref in _SCHEMA_CACHE:
|
|
31
|
+
return _SCHEMA_CACHE[schema_ref]
|
|
32
|
+
|
|
33
|
+
parsed = urlparse(schema_ref)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
if parsed.scheme in ("http", "https", "file"):
|
|
37
|
+
with urlopen(schema_ref) as resp:
|
|
38
|
+
schema = json.load(resp)
|
|
39
|
+
else:
|
|
40
|
+
schema_path = (base_path.parent / schema_ref).resolve()
|
|
41
|
+
if not schema_path.exists():
|
|
42
|
+
raise PolicyError(f"Schema not found: {schema_path}")
|
|
43
|
+
schema = json.loads(schema_path.read_text())
|
|
44
|
+
except Exception as e:
|
|
45
|
+
raise PolicyError(f"Failed to load schema '{schema_ref}': {e}") from e
|
|
46
|
+
|
|
47
|
+
_SCHEMA_CACHE[schema_ref] = schema
|
|
48
|
+
return schema
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ModPolicy:
|
|
52
|
+
"""
|
|
53
|
+
Enforces mod compatibility rules:
|
|
54
|
+
- removes conflicts
|
|
55
|
+
- injects recommended sub-mods
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, policy_path: str | Path = "configs/policy.json"):
|
|
59
|
+
self.policy_path = policy_path if isinstance(policy_path, Path) else Path(policy_path)
|
|
60
|
+
self.rules: NormalizedPolicyRules = {}
|
|
61
|
+
self.schema_ref: str | None = None
|
|
62
|
+
|
|
63
|
+
self._load()
|
|
64
|
+
self._validate()
|
|
65
|
+
self._normalize()
|
|
66
|
+
|
|
67
|
+
# ---------- loading & validation ----------
|
|
68
|
+
|
|
69
|
+
def _load(self) -> None:
|
|
70
|
+
try:
|
|
71
|
+
raw = json.loads(self.policy_path.read_text())
|
|
72
|
+
|
|
73
|
+
self.schema_ref = raw.get("$schema")
|
|
74
|
+
if not self.schema_ref:
|
|
75
|
+
raise PolicyError("Policy file missing $schema field")
|
|
76
|
+
|
|
77
|
+
raw.pop("$schema", None)
|
|
78
|
+
self.rules = raw
|
|
79
|
+
|
|
80
|
+
del raw
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise PolicyError(f"Failed to load policy: {e}") from e
|
|
83
|
+
|
|
84
|
+
def _validate(self) -> None:
|
|
85
|
+
try:
|
|
86
|
+
schema = _load_schema(self.schema_ref, self.policy_path)
|
|
87
|
+
validate(instance=self.rules, schema=schema)
|
|
88
|
+
del schema
|
|
89
|
+
except ValidationError as e:
|
|
90
|
+
raise PolicyError(f"Policy schema violation:\n{e.message}") from e
|
|
91
|
+
except Exception as e:
|
|
92
|
+
raise PolicyError(f"Schema validation failed: {e}") from e
|
|
93
|
+
|
|
94
|
+
def _normalize(self) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Normalize rule values into sets for O(1) lookups
|
|
97
|
+
"""
|
|
98
|
+
for rule in self.rules.values():
|
|
99
|
+
rule["conflicts"] = set(rule.get("conflicts", []))
|
|
100
|
+
rule["sub_mods"] = set(rule.get("sub_mods", []))
|
|
101
|
+
|
|
102
|
+
# ---------- public API ----------
|
|
103
|
+
|
|
104
|
+
def apply(self, mods: Iterable[str]) -> Set[str]:
|
|
105
|
+
"""
|
|
106
|
+
Apply policy to a mod set.
|
|
107
|
+
Recursively adds sub-mods and removes conflicts.
|
|
108
|
+
Explicit mods always win over implicit ones.
|
|
109
|
+
"""
|
|
110
|
+
explicit: Set[str] = set(mods)
|
|
111
|
+
active: Set[str] = set(explicit)
|
|
112
|
+
implicit: Set[str] = set()
|
|
113
|
+
|
|
114
|
+
queue = deque(active)
|
|
115
|
+
|
|
116
|
+
# 1. Expand sub-mods recursively
|
|
117
|
+
while queue:
|
|
118
|
+
current = queue.popleft()
|
|
119
|
+
rule = self.rules.get(current)
|
|
120
|
+
if not rule:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
for sub in rule["sub_mods"]:
|
|
124
|
+
if sub not in active:
|
|
125
|
+
active.add(sub)
|
|
126
|
+
implicit.add(sub)
|
|
127
|
+
queue.append(sub)
|
|
128
|
+
|
|
129
|
+
# 2. Resolve conflicts (implicit loses first)
|
|
130
|
+
for mod in sorted(active):
|
|
131
|
+
rule = self.rules.get(mod)
|
|
132
|
+
if not rule:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
for conflict in rule["conflicts"]:
|
|
136
|
+
if conflict in active:
|
|
137
|
+
if conflict in explicit and mod in explicit:
|
|
138
|
+
raise PolicyError(
|
|
139
|
+
f"Explicit mod conflict: {mod} ↔ {conflict}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if conflict in implicit:
|
|
143
|
+
active.remove(conflict)
|
|
144
|
+
implicit.discard(conflict)
|
|
145
|
+
|
|
146
|
+
del queue, explicit, implicit
|
|
147
|
+
return active
|
|
148
|
+
|
|
149
|
+
def diff(self, mods: Iterable[str]) -> Dict[str, List[str]]:
|
|
150
|
+
"""
|
|
151
|
+
Show what would change without applying.
|
|
152
|
+
"""
|
|
153
|
+
original = set(mods)
|
|
154
|
+
final = self.apply(mods)
|
|
155
|
+
|
|
156
|
+
diff = {
|
|
157
|
+
"added": sorted(final - original),
|
|
158
|
+
"removed": sorted(original - final),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
del original, final
|
|
162
|
+
return diff
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from typing import Iterable, Set
|
|
2
|
+
from collections import deque
|
|
3
|
+
|
|
4
|
+
from modforge_cli.core.policy import ModPolicy
|
|
5
|
+
from modforge_cli.core.models import SearchResult, ProjectVersionList, ProjectVersion
|
|
6
|
+
from modforge_cli.api import ModrinthAPIConfig
|
|
7
|
+
from requests import get
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from modforge_cli.__version__ import __version__, __author__
|
|
12
|
+
except ImportError:
|
|
13
|
+
__version__ = "unknown"
|
|
14
|
+
__author__ = "Frank1o3"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ModResolver:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
policy: ModPolicy,
|
|
22
|
+
api: ModrinthAPIConfig,
|
|
23
|
+
mc_version: str,
|
|
24
|
+
loader: str,
|
|
25
|
+
) -> None:
|
|
26
|
+
self.policy = policy
|
|
27
|
+
self.api = api
|
|
28
|
+
self.mc_version = mc_version
|
|
29
|
+
self.loader = loader
|
|
30
|
+
|
|
31
|
+
self._headers = {
|
|
32
|
+
"User-Agent": f"{__author__}/ModForge-CLI/{__version__}"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def _select_version(self, versions: list[ProjectVersion]) -> ProjectVersion | None:
|
|
36
|
+
"""
|
|
37
|
+
Prefer:
|
|
38
|
+
1. Release versions
|
|
39
|
+
2. Matching MC + loader
|
|
40
|
+
"""
|
|
41
|
+
for v in versions:
|
|
42
|
+
if (
|
|
43
|
+
v.is_release
|
|
44
|
+
and self.mc_version in v.game_versions
|
|
45
|
+
and self.loader in v.loaders
|
|
46
|
+
):
|
|
47
|
+
return v
|
|
48
|
+
|
|
49
|
+
for v in versions:
|
|
50
|
+
if (
|
|
51
|
+
self.mc_version in v.game_versions
|
|
52
|
+
and self.loader in v.loaders
|
|
53
|
+
):
|
|
54
|
+
return v
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def resolve(self, mods: Iterable[str]) -> Set[str]:
|
|
59
|
+
expanded = self.policy.apply(mods)
|
|
60
|
+
|
|
61
|
+
resolved: Set[str] = set()
|
|
62
|
+
queue: deque[str] = deque()
|
|
63
|
+
|
|
64
|
+
search_cache: dict[str, str | None] = {}
|
|
65
|
+
version_cache: dict[str, list[ProjectVersion]] = {}
|
|
66
|
+
|
|
67
|
+
# ---- Phase 1: slug → project_id ----
|
|
68
|
+
for slug in expanded:
|
|
69
|
+
if slug not in search_cache:
|
|
70
|
+
url = self.api.search(
|
|
71
|
+
slug,
|
|
72
|
+
game_versions=[self.mc_version],
|
|
73
|
+
loaders=[self.loader],
|
|
74
|
+
)
|
|
75
|
+
response = get(url, headers=self._headers)
|
|
76
|
+
data = SearchResult.model_validate_json(response.text)
|
|
77
|
+
|
|
78
|
+
project_id = None
|
|
79
|
+
for hit in data.hits:
|
|
80
|
+
if hit.project_type != "mod":
|
|
81
|
+
continue
|
|
82
|
+
if self.mc_version not in hit.versions:
|
|
83
|
+
continue
|
|
84
|
+
project_id = hit.project_id
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
search_cache[slug] = project_id
|
|
88
|
+
del url, response, data
|
|
89
|
+
|
|
90
|
+
project_id = search_cache[slug]
|
|
91
|
+
if project_id and project_id not in resolved:
|
|
92
|
+
resolved.add(project_id)
|
|
93
|
+
queue.append(project_id)
|
|
94
|
+
|
|
95
|
+
# ---- Phase 2: dependency resolution ----
|
|
96
|
+
while queue:
|
|
97
|
+
pid = queue.popleft()
|
|
98
|
+
|
|
99
|
+
if pid not in version_cache:
|
|
100
|
+
url = self.api.project_versions(pid)
|
|
101
|
+
response = get(url, headers=self._headers)
|
|
102
|
+
versions = ProjectVersionList.validate_json(response.text)
|
|
103
|
+
version_cache[pid] = versions
|
|
104
|
+
del url, response
|
|
105
|
+
else:
|
|
106
|
+
versions = version_cache[pid]
|
|
107
|
+
|
|
108
|
+
version = self._select_version(versions)
|
|
109
|
+
if not version:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
for dep in version.dependencies:
|
|
113
|
+
dtype = dep.dependency_type
|
|
114
|
+
dep_id = dep.project_id
|
|
115
|
+
|
|
116
|
+
if not dep_id:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
if dtype == "incompatible":
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
f"Incompatible dependency detected: {pid} ↔ {dep_id}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if dtype in ("required", "optional"):
|
|
125
|
+
if dep_id not in resolved:
|
|
126
|
+
resolved.add(dep_id)
|
|
127
|
+
queue.append(dep_id)
|
|
128
|
+
|
|
129
|
+
# embedded deps are intentionally ignored
|
|
130
|
+
|
|
131
|
+
del versions, version, pid
|
|
132
|
+
|
|
133
|
+
del queue, expanded, search_cache, version_cache
|
|
134
|
+
return resolved
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modforge-cli
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: ModForge-CLI — a Modrinth-based Minecraft modpack builder
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Frank1o3
|
|
8
|
+
Author-email: jahdy1o3@gmail.com
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Requires-Dist: aiofiles (>=25.1.0,<26.0.0)
|
|
21
|
+
Requires-Dist: aiohttp (>=3.13.3,<4.0.0)
|
|
22
|
+
Requires-Dist: jsonschema (>=4.25.1,<5.0.0)
|
|
23
|
+
Requires-Dist: pydantic (>=2.12.5,<3.0.0)
|
|
24
|
+
Requires-Dist: pyfiglet (>=1.0.4,<2.0.0)
|
|
25
|
+
Requires-Dist: pyzipper (>=0.3.6,<0.4.0)
|
|
26
|
+
Requires-Dist: requests (>=2.32.5,<3.0.0)
|
|
27
|
+
Requires-Dist: rich (>=14.2.0,<15.0.0)
|
|
28
|
+
Requires-Dist: typer (>=0.21.1,<0.22.0)
|
|
29
|
+
Project-URL: Homepage, https://frank1o3.github.io/ModForge-CLI/
|
|
30
|
+
Project-URL: Repository, https://github.com/Frank1o3/ModForge-CLI
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# ModForge-CLI ⛏
|
|
34
|
+
|
|
35
|
+
[](https://www.python.org/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
[](https://docs.modrinth.com/api-spec)
|
|
38
|
+
|
|
39
|
+
**ModForge-CLI** is a powerful CLI tool for building and managing custom Minecraft modpacks using the Modrinth API v2.
|
|
40
|
+
|
|
41
|
+
Search for projects, fetch versions, validate manifests, download mods with hash checks, and generate complete files — all from the terminal.
|
|
42
|
+
|
|
43
|
+
Ideal for modpack developers, server admins, and automation scripts.
|
|
44
|
+
|
|
45
|
+
## Terminal Banner
|
|
46
|
+
|
|
47
|
+
When you run ModForge-CLI, you'll be greeted with this colorful Minecraft-themed banner
|
|
48
|
+
|
|
49
|
+
## Key Features
|
|
50
|
+
|
|
51
|
+
- **Modrinth API v2 Integration**: Search projects, list versions, fetch metadata in bulk.
|
|
52
|
+
- **Modpack Management**: Read/validate `modrinth.index.json`, build packs from metadata.
|
|
53
|
+
- **Validation**: Full JSON Schema checks + optional Pydantic models for strict typing.
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Requires **Python 3.13+**.
|
|
58
|
+
|
|
59
|
+
**Recommended (Poetry)**:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
poetry install
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Alternative (pip)**:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install -r requirements.txt
|
|
69
|
+
```
|
|
70
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
modforge_cli/__init__.py,sha256=vfUV68-GslDHLhtm2mME6LC2_9lXqFLKNGylG54A-fQ,102
|
|
2
|
+
modforge_cli/__main__.py,sha256=1atZnKiKAP03RIuBu9nl2XQioTQFjfaeq5O4OfsQNto,133
|
|
3
|
+
modforge_cli/__version__.py,sha256=ksmUbJNIJvzvPWdmuIuY2Q1VcoeYHpVGSoC-JbAKCqk,88
|
|
4
|
+
modforge_cli/api/__init__.py,sha256=2Yv8rwwndMqnYOHNWkPPdYhPGItN04w1vhx-oDId8K0,137
|
|
5
|
+
modforge_cli/api/modrinth.py,sha256=vOiPld6DyPJuFxcmsc6CxmqwbdT4XL7smRsu2TrS8DA,7883
|
|
6
|
+
modforge_cli/cli.py,sha256=SU9dSTYubTsHQ1t6e_m0G6HgW7zF3h1TKof10r-xT4Q,17411
|
|
7
|
+
modforge_cli/core/__init__.py,sha256=_MWEHe9Kqeq93g6WXxL9Jy8jS0-eDdtdrbHHpV1O91Q,318
|
|
8
|
+
modforge_cli/core/downloader.py,sha256=L0Y7zosFSSaLIDRHeBiuyfWnByKV6OedxUCE1fG1Zr0,2608
|
|
9
|
+
modforge_cli/core/models.py,sha256=a-8Ua7rxDo6jChSPK4whPD13Kj2mF89IAArX24rvddc,1828
|
|
10
|
+
modforge_cli/core/policy.py,sha256=LdQ005muAAuawTD6AYjqNDWUkWnc5fdgJMEqlBmRsjM,4810
|
|
11
|
+
modforge_cli/core/resolver.py,sha256=fhVDl1zTJTD4AkWko8y1H2m1ArGnSUQyogaEJ925oCA,4076
|
|
12
|
+
modforge_cli-0.1.5.dist-info/METADATA,sha256=7GHMur_0ZujqKWvLZHt1D6IRRKZxR8W5eh5HgH8rPos,2468
|
|
13
|
+
modforge_cli-0.1.5.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
14
|
+
modforge_cli-0.1.5.dist-info/entry_points.txt,sha256=JxVNWD_fF0JwwJ_OTdfCYZ5-1mAFH0XqUSiARvf5oRA,55
|
|
15
|
+
modforge_cli-0.1.5.dist-info/licenses/LICENSE,sha256=SAuHlb0YymKIXKAXl0lwgwwu31REY-3oBdLARIObBbs,1065
|
|
16
|
+
modforge_cli-0.1.5.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Frank1o3
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|