packsmith 0.1.0__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.
@@ -0,0 +1,24 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+ indent_style = space
9
+ indent_size = 4
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
13
+
14
+ [*.json]
15
+ indent_size = 2
16
+
17
+ [*.yml]
18
+ indent_size = 2
19
+
20
+ [*.yaml]
21
+ indent_size = 2
22
+
23
+ [*.toml]
24
+ indent_size = 4
packsmith-0.1.0/.env ADDED
@@ -0,0 +1 @@
1
+ UV_LINK_MODE=copy
@@ -0,0 +1,53 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ tags:
7
+ - "v*"
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Check out code
17
+ uses: actions/checkout@v7
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v6.3.0
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Install uv
25
+ uses: astral-sh/setup-uv@v8.2.0
26
+ with:
27
+ version: latest
28
+
29
+ - name: Build distribution
30
+ run: uv build
31
+
32
+ - name: Upload build artifacts
33
+ uses: actions/upload-artifact@v7.0.1
34
+ with:
35
+ name: dist
36
+ path: dist/
37
+
38
+ publish:
39
+ needs: build
40
+ runs-on: ubuntu-latest
41
+ environment:
42
+ name: pypi
43
+ url: https://pypi.org/p/packsmith
44
+ permissions:
45
+ id-token: write
46
+ steps:
47
+ - name: Download all the dists
48
+ uses: actions/download-artifact@v8.0.1
49
+ with:
50
+ name: dist
51
+ path: dist/
52
+ - name: Publish distribution 📦 to PyPI
53
+ uses: pypa/gh-action-pypi-publish@release/v1.14
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1,18 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.15.20
4
+ hooks:
5
+ - id: ruff-check
6
+ - id: ruff-format
7
+
8
+ - repo: https://github.com/astral-sh/ty-pre-commit
9
+ rev: v0.0.55
10
+ hooks:
11
+ - id: ty
12
+
13
+ - repo: https://github.com/pre-commit/pre-commit-hooks
14
+ rev: v6.0.0
15
+ hooks:
16
+ - id: check-yaml
17
+ - id: end-of-file-fixer
18
+ - id: trailing-whitespace
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,32 @@
1
+ {
2
+ "editor.formatOnSave": true,
3
+ "editor.codeActionsOnSave": {
4
+ "source.fixAll.ruff": "always",
5
+ "source.organizeImports.ruff": "always"
6
+ },
7
+ "[python]": {
8
+ "editor.defaultFormatter": "charliermarsh.ruff"
9
+ },
10
+ "python.analysis.typeCheckingMode": "off",
11
+ "files.trimTrailingWhitespace": true,
12
+ "files.insertFinalNewline": true,
13
+ "editor.rulers": [
14
+ 88
15
+ ],
16
+ "editor.tabSize": 4,
17
+ "editor.insertSpaces": true,
18
+ "python.testing.pytestEnabled": true,
19
+ "python.testing.unittestEnabled": false,
20
+ "python.testing.pytestArgs": [
21
+ "tests"
22
+ ],
23
+ "python.terminal.activateEnvironment": true,
24
+ "files.exclude": {
25
+ "**/.git": true,
26
+ "**/.svn": true,
27
+ "**/.hg": true,
28
+ "**/.DS_Store": true,
29
+ "**/Thumbs.db": true,
30
+ "**/__pycache__": true
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 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.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: packsmith
3
+ Version: 0.1.0
4
+ Summary: A CLI based minecraft mod pack creator/manager
5
+ Project-URL: Homepage, https://github.com/Frank1o3/packsmith
6
+ Project-URL: Repository, https://github.com/Frank1o3/packsmith
7
+ Project-URL: Issues, https://github.com/Frank1o3/packsmith/issues
8
+ Author: Frank1o3
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,curseforge,minecraft,modpack,modrinth
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Games/Entertainment
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.14
22
+ Requires-Dist: httpx>=0.28.1
23
+ Requires-Dist: pydantic>=2.13.4
24
+ Requires-Dist: pyfiglet>=1.0.4
25
+ Requires-Dist: tomli-w>=1.2.0
26
+ Requires-Dist: typer>=0.26.8
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Packsmith
30
+
31
+ Packsmith is a CLI tool for creating and managing Minecraft modpacks. It is designed to help you initialize a pack, add mods or other content, resolve dependencies from Modrinth, download the resolved files, and export a distributable modpack archive.
32
+
33
+ This project is a remake of [ModForge-CLI](https://github.com/Frank1o3/ModForge-CLI), with a focus on a cleaner codebase and a more direct workflow for modern modpack creation.
34
+
35
+ ## What Packsmith does
36
+
37
+ Packsmith helps you:
38
+
39
+ - create a new modpack structure with the expected folders
40
+ - add mods, resourcepacks, and shaders to a pack manifest
41
+ - resolve Modrinth dependencies into a lockfile
42
+ - download the resolved files into the correct directories
43
+ - export a modpack as an `.mrpack`-style archive for distribution
44
+
45
+ ## Installation
46
+
47
+ You can install the project locally with:
48
+
49
+ ```bash
50
+ pip install -e .
51
+ ```
52
+
53
+ Or with `uv`:
54
+
55
+ ```bash
56
+ uv sync
57
+ ```
58
+
59
+ ## Basic workflow
60
+
61
+ 1. Create a new pack:
62
+
63
+ ```bash
64
+ packsmith init <pack-name> <game-version> <loader> <loader-version>
65
+ ```
66
+
67
+ 2. Add content to the pack:
68
+
69
+ ```bash
70
+ packsmith add <name> mod
71
+ ```
72
+
73
+ You can also use other project types such as `resourcepack` or `shader`.
74
+
75
+ 3. Resolve dependencies:
76
+
77
+ ```bash
78
+ packsmith resolve
79
+ ```
80
+
81
+ 4. Download the resolved files:
82
+
83
+ ```bash
84
+ packsmith download
85
+ ```
86
+
87
+ 5. Export the pack:
88
+
89
+ ```bash
90
+ packsmith export --client
91
+ ```
92
+
93
+ or
94
+
95
+ ```bash
96
+ packsmith export --server
97
+ ```
98
+
99
+ by default it will export the pack with both server and client stuff
100
+
101
+ ## Project structure
102
+
103
+ When you initialize a pack, Packsmith creates a basic folder layout similar to this:
104
+
105
+ ```text
106
+ <pack-name>/
107
+ meta.json
108
+ lock.toml
109
+ mods/
110
+ overrides/
111
+ config/
112
+ resourcepacks/
113
+ shaderpacks/
114
+ ```
115
+
116
+ ## Notes
117
+
118
+ Packsmith is still being developed, and some features are still evolving. The current focus is on building a reliable workflow for modpack creation, dependency resolution, and export.
119
+
120
+ And i am planning on adding a command that takes in you minecraft version and loader and will give you back the latest most compatible loader for that version of the game
121
+
122
+ ## Mention
123
+ You can look at the [test.sh](test.sh) file in the repos root the code in it makes a basic modpack and you can use it as reference
@@ -0,0 +1,95 @@
1
+ # Packsmith
2
+
3
+ Packsmith is a CLI tool for creating and managing Minecraft modpacks. It is designed to help you initialize a pack, add mods or other content, resolve dependencies from Modrinth, download the resolved files, and export a distributable modpack archive.
4
+
5
+ This project is a remake of [ModForge-CLI](https://github.com/Frank1o3/ModForge-CLI), with a focus on a cleaner codebase and a more direct workflow for modern modpack creation.
6
+
7
+ ## What Packsmith does
8
+
9
+ Packsmith helps you:
10
+
11
+ - create a new modpack structure with the expected folders
12
+ - add mods, resourcepacks, and shaders to a pack manifest
13
+ - resolve Modrinth dependencies into a lockfile
14
+ - download the resolved files into the correct directories
15
+ - export a modpack as an `.mrpack`-style archive for distribution
16
+
17
+ ## Installation
18
+
19
+ You can install the project locally with:
20
+
21
+ ```bash
22
+ pip install -e .
23
+ ```
24
+
25
+ Or with `uv`:
26
+
27
+ ```bash
28
+ uv sync
29
+ ```
30
+
31
+ ## Basic workflow
32
+
33
+ 1. Create a new pack:
34
+
35
+ ```bash
36
+ packsmith init <pack-name> <game-version> <loader> <loader-version>
37
+ ```
38
+
39
+ 2. Add content to the pack:
40
+
41
+ ```bash
42
+ packsmith add <name> mod
43
+ ```
44
+
45
+ You can also use other project types such as `resourcepack` or `shader`.
46
+
47
+ 3. Resolve dependencies:
48
+
49
+ ```bash
50
+ packsmith resolve
51
+ ```
52
+
53
+ 4. Download the resolved files:
54
+
55
+ ```bash
56
+ packsmith download
57
+ ```
58
+
59
+ 5. Export the pack:
60
+
61
+ ```bash
62
+ packsmith export --client
63
+ ```
64
+
65
+ or
66
+
67
+ ```bash
68
+ packsmith export --server
69
+ ```
70
+
71
+ by default it will export the pack with both server and client stuff
72
+
73
+ ## Project structure
74
+
75
+ When you initialize a pack, Packsmith creates a basic folder layout similar to this:
76
+
77
+ ```text
78
+ <pack-name>/
79
+ meta.json
80
+ lock.toml
81
+ mods/
82
+ overrides/
83
+ config/
84
+ resourcepacks/
85
+ shaderpacks/
86
+ ```
87
+
88
+ ## Notes
89
+
90
+ Packsmith is still being developed, and some features are still evolving. The current focus is on building a reliable workflow for modpack creation, dependency resolution, and export.
91
+
92
+ And i am planning on adding a command that takes in you minecraft version and loader and will give you back the latest most compatible loader for that version of the game
93
+
94
+ ## Mention
95
+ You can look at the [test.sh](test.sh) file in the repos root the code in it makes a basic modpack and you can use it as reference
@@ -0,0 +1,64 @@
1
+ [project]
2
+ name = "packsmith"
3
+ version = "0.1.0"
4
+ description = "A CLI based minecraft mod pack creator/manager"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ license = "MIT"
8
+
9
+ dependencies = [
10
+ "httpx>=0.28.1",
11
+ "pydantic>=2.13.4",
12
+ "pyfiglet>=1.0.4",
13
+ "tomli-w>=1.2.0",
14
+ "typer>=0.26.8",
15
+ ]
16
+
17
+ authors = [
18
+ { name = "Frank1o3" }
19
+ ]
20
+
21
+ keywords = [
22
+ "minecraft",
23
+ "modrinth",
24
+ "curseforge",
25
+ "modpack",
26
+ "cli",
27
+ ]
28
+
29
+ classifiers = [
30
+ "Development Status :: 3 - Alpha",
31
+ "Environment :: Console",
32
+ "Intended Audience :: Developers",
33
+ "Intended Audience :: End Users/Desktop",
34
+ "License :: OSI Approved :: MIT License",
35
+ "Programming Language :: Python :: 3.14",
36
+ "Programming Language :: Python :: 3 :: Only",
37
+ "Topic :: Games/Entertainment",
38
+ "Topic :: Utilities",
39
+ ]
40
+
41
+ [build-system]
42
+ requires = ["hatchling"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "pre-commit>=4.6.0",
48
+ "pytest>=9.1.1",
49
+ "pytest-cov>=7.1.0",
50
+ "pytest-mock>=3.15.1",
51
+ "ruff>=0.15.20",
52
+ "ty>=0.0.55",
53
+ ]
54
+
55
+ [project.scripts]
56
+ packsmith = "packsmith.main:main"
57
+
58
+ [project.urls]
59
+ Homepage = "https://github.com/Frank1o3/packsmith"
60
+ Repository = "https://github.com/Frank1o3/packsmith"
61
+ Issues = "https://github.com/Frank1o3/packsmith/issues"
62
+
63
+ [tool.uv]
64
+ package = true
@@ -0,0 +1,63 @@
1
+ target-version = "py314"
2
+ line-length = 88
3
+ src = ["src"]
4
+
5
+ preview = true
6
+ unsafe-fixes = false
7
+
8
+ exclude = [
9
+ ".git",
10
+ ".venv",
11
+ ".pytest_cache",
12
+ ".ruff_cache",
13
+ "__pycache__",
14
+ "build",
15
+ "dist",
16
+ ]
17
+
18
+ [lint]
19
+ select = ["ALL"]
20
+
21
+ ignore = [
22
+ # TODOs
23
+ "TD002",
24
+ "TD003",
25
+ "FIX002",
26
+
27
+ # Docstrings (enable gradually)
28
+ "D100",
29
+ "D101",
30
+ "D102",
31
+ "D103",
32
+ "D104",
33
+ "D105",
34
+ "D106",
35
+ "D107",
36
+ "D203",
37
+ "D205",
38
+ "D213",
39
+
40
+ # Tests
41
+ "S101",
42
+
43
+ # Allow Any when needed
44
+ "ANN401",
45
+
46
+ # Security exceptions
47
+ "S324",
48
+
49
+ # Formatter compatibility
50
+ "COM812",
51
+ "ISC001",
52
+ ]
53
+
54
+ dummy-variable-rgx = "^(_+|(_[A-Za-z0-9_]*[A-Za-z0-9]+?))$"
55
+
56
+ [lint.per-file-ignores]
57
+ "tests/**/*.py" = ["S101", "D", "PLR2004"]
58
+
59
+ [format]
60
+ quote-style = "double"
61
+ indent-style = "space"
62
+ line-ending = "lf"
63
+ docstring-code-format = true
@@ -0,0 +1,2 @@
1
+ # Copyright (c) 2026 Frank1o3
2
+ # SPDX-License-Identifier: MIT
@@ -0,0 +1,6 @@
1
+ # Copyright (c) 2026 Frank1o3
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from .main import main
5
+
6
+ main()
@@ -0,0 +1,5 @@
1
+ # Copyright (c) 2026 Frank1o3
2
+ # SPDX-License-Identifier: MIT
3
+ from .modrinth import ModrinthClient
4
+
5
+ __all__ = ["ModrinthClient"]
@@ -0,0 +1,140 @@
1
+ # Copyright (c) 2026 Frank1o3
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import json
8
+ import logging
9
+ import time
10
+ from collections.abc import Mapping, Sequence
11
+ from importlib.metadata import version
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from httpx import AsyncClient, QueryParams, Response
15
+ from pydantic import BaseModel
16
+
17
+ from packsmith.core.models import Hit, ProjectVersion, ProjectVersions, Search
18
+
19
+ if TYPE_CHECKING:
20
+ from packsmith.core.models import ProjectType
21
+
22
+ PrimitiveData = str | int | float | bool | None
23
+
24
+ QueryParamTypes = (
25
+ QueryParams
26
+ | Mapping[str, PrimitiveData | Sequence[PrimitiveData]]
27
+ | list[tuple[str, PrimitiveData]]
28
+ | tuple[tuple[str, PrimitiveData], ...]
29
+ | str
30
+ | bytes
31
+ ) | None
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class RateLimitState(BaseModel):
37
+ limit: int | None = None
38
+ remaining: int | None = None
39
+ reset_after: float | None = None
40
+ last_update: float | None = None
41
+
42
+
43
+ class ModrinthClient:
44
+ BASE_URL = "https://api.modrinth.com/v2"
45
+
46
+ def __init__(self) -> None:
47
+ user_agent = f"Frank1o3/packsmith/{version('packsmith')} (https://github.com/Frank1o3/packsmith)"
48
+
49
+ self._client = AsyncClient(
50
+ base_url=self.BASE_URL, headers={"User-Agent": user_agent}, timeout=30.0
51
+ )
52
+ self.rate = RateLimitState()
53
+ self._lock = asyncio.Lock()
54
+
55
+ async def get(
56
+ self,
57
+ path: str,
58
+ *,
59
+ params: QueryParamTypes = None,
60
+ ) -> Any:
61
+ async with self._lock:
62
+ await self._respect_rate_limit()
63
+
64
+ resp = await self._client.get(path, params=params)
65
+ self._update_rate_limit(resp)
66
+ resp.raise_for_status()
67
+
68
+ return resp.json()
69
+
70
+ async def search(
71
+ self, name: str, project_type: ProjectType, loader: str, game_version: str
72
+ ) -> Search:
73
+ facets = [
74
+ [f"project_type:{project_type}"],
75
+ [f"versions:{game_version}"],
76
+ ]
77
+ params = {
78
+ "query": name,
79
+ "limit": 15,
80
+ "facets": json.dumps(facets),
81
+ }
82
+ if project_type == "mod":
83
+ facets.append([f"categories:{loader}"])
84
+ return Search.model_validate(await self.get("/search", params=params))
85
+
86
+ async def get_project(self, project_id: str) -> Hit:
87
+ endpoint = f"/project/{project_id}/"
88
+ return Hit.model_validate_json(await self.get(endpoint))
89
+
90
+ async def get_project_versions(self, project_id: str) -> list[ProjectVersion]:
91
+ endpoint = f"/project/{project_id}/version"
92
+ return ProjectVersions.validate_python(await self.get(endpoint))
93
+
94
+ # ------------------------
95
+ # rate limit logic
96
+ # ------------------------
97
+ def _update_rate_limit(self, resp: Response) -> None:
98
+ headers = resp.headers
99
+
100
+ self.rate.limit = self._safe_int(headers.get("X-Ratelimit-Limit"))
101
+ self.rate.remaining = self._safe_int(headers.get("X-Ratelimit-Remaining"))
102
+
103
+ reset = headers.get("X-Ratelimit-Reset")
104
+ if reset is not None:
105
+ self.rate.reset_after = self._safe_float(reset)
106
+ self.rate.last_update = time.time()
107
+
108
+ async def _respect_rate_limit(self) -> None:
109
+ if (
110
+ self.rate.remaining is not None
111
+ and self.rate.remaining <= 0
112
+ and self.rate.reset_after is not None
113
+ and self.rate.last_update is not None
114
+ ):
115
+ wait_time = self.rate.reset_after - (time.time() - self.rate.last_update)
116
+ if wait_time > 0:
117
+ await asyncio.sleep(wait_time)
118
+
119
+ async def close(self) -> None:
120
+ await self._client.aclose()
121
+
122
+ async def __aexit__(self, *_: object) -> None:
123
+ await self.close()
124
+
125
+ # ------------------------
126
+ # Static methods
127
+ # ------------------------
128
+ @staticmethod
129
+ def _safe_int(value: str | None) -> int | None:
130
+ try:
131
+ return int(value) if value is not None else None
132
+ except ValueError:
133
+ return None
134
+
135
+ @staticmethod
136
+ def _safe_float(value: str | None) -> float | None:
137
+ try:
138
+ return float(value) if value is not None else None
139
+ except ValueError:
140
+ return None
@@ -0,0 +1,2 @@
1
+ # Copyright (c) 2026 Frank1o3
2
+ # SPDX-License-Identifier: MIT