modpack-lock 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/index.js +213 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 N. Escobar
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
<a href="https://github.com/nickesc/modpack-lock"><img alt="Source: Github" src="https://img.shields.io/badge/source-github-brightgreen?style=for-the-badge&logo=github&labelColor=%23505050"></a>
|
|
3
|
+
<a href="https://www.npmjs.com/package/modpack-lock"><img alt="NPM: npmjs.com/package/modpack-lock" src="https://img.shields.io/npm/v/modpack-lock?style=for-the-badge&logo=npm&logoColor=white&label=npm&color=%23C12127&labelColor=%23505050"></a>
|
|
4
|
+
|
|
5
|
+
# modpack-lock
|
|
6
|
+
|
|
7
|
+
###### by nickesc - [GitHub](https://github.com/nickesc) | [Modrinth](https://modrinth.com/user/nickesc)
|
|
8
|
+
|
|
9
|
+
Creates a modpack lockfile for files hosted on Modrinth (mods, resource packs, shaders and datapacks).
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Many mod and pack authors request that modpack creators link to Modrinth or CurseForge downloads rather than re-hosting files. This makes it difficult to track content files in version control when pushing to a remote server.
|
|
15
|
+
|
|
16
|
+
This script generates a `modpack.lock` file in the current directory containing a JSON object with a plaintext representation of the modpack's contents. This object contains the metadata for the content available on Modrinth, including hashes, versions, names, download URLs and more. This allows for easy diffing and clear version history.
|
|
17
|
+
|
|
18
|
+
> While an `.mrpack` file could be used to track changes to the modpack, it is a large, binary file that cannot be diffed and can contain large amounts of duplicate data from the rest of the repository.
|
|
19
|
+
|
|
20
|
+
The lockfile could also serve as a basis for restoring modpack contents after cloning the repository to a new machine.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
To install the script globally with `npm`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g modpack-lock
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Alternatively, you can run it using `npx`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx modpack-lock
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Navigate to your Minecraft profile directory (the folder containing `mods`, `resourcepacks`, `datapacks`, and `shaderpacks` folders) and run:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
modpack-lock
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The script will:
|
|
45
|
+
|
|
46
|
+
1. Scan the `mods`, `resourcepacks`, `datapacks`, and `shaderpacks` directories for `.jar` and `.zip` files
|
|
47
|
+
2. Calculate SHA1 hashes for each file
|
|
48
|
+
3. Query the Modrinth API to find version information
|
|
49
|
+
4. Generate a `modpack.lock` file in the current directory
|
|
50
|
+
|
|
51
|
+
Then, commit the `modpack.lock` file to your repository and push it to your remote.
|
|
52
|
+
|
|
53
|
+
> [!TIP]
|
|
54
|
+
> You can run this script as a pre-commit hook to ensure that the modpack lockfile is up to date before committing your changes to your repository.
|
|
55
|
+
>
|
|
56
|
+
> Also, consider adding these rules to your `.gitignore` to ensure you don't commit the modpack contents to your repository, with exceptions for any files that are not Modrinth-hosted:
|
|
57
|
+
>
|
|
58
|
+
> ```txt
|
|
59
|
+
> mods/*.jar
|
|
60
|
+
> resourcepacks/*.zip
|
|
61
|
+
> datapacks/*.zip
|
|
62
|
+
> shaderpacks/*.zip
|
|
63
|
+
>
|
|
64
|
+
> ## Exceptions
|
|
65
|
+
> # !mods/example.jar
|
|
66
|
+
> ```
|
|
67
|
+
|
|
68
|
+
## Output
|
|
69
|
+
|
|
70
|
+
The `modpack.lock` file has the following structure:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"version": "1.0.0",
|
|
75
|
+
"generated": "2026-01-06T03:00:00.000Z",
|
|
76
|
+
"dependencies": {
|
|
77
|
+
"mods": [
|
|
78
|
+
{
|
|
79
|
+
"path": "mods/example-mod.jar",
|
|
80
|
+
"version": { ... }
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"resourcepacks": [ ... ],
|
|
84
|
+
"datapacks": [ ... ],
|
|
85
|
+
"shaderpacks": [ ... ]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Future Plans
|
|
91
|
+
|
|
92
|
+
- [ ] Add support for CurseForge
|
|
93
|
+
- [ ] Add support for restoring modpack contents using the lockfile
|
|
94
|
+
- [ ] Add CLI option for dry-run
|
|
95
|
+
- [ ] Add CLI option for verbose output
|
|
96
|
+
- [ ] Add CLI option for quiet output
|
|
97
|
+
- [ ] Add CLI option for printout of rules to add to .gitignore (the files that are not hosted)
|
|
98
|
+
|
|
99
|
+
Feel free to submit a pull request working on any of the above, or open an issue for any feature requests or bug reports.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for more details.
|
package/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
const LOCKFILE_VERSION = '1.0.0';
|
|
8
|
+
const MODPACK_LOCKFILE_NAME = 'modpack.lock';
|
|
9
|
+
const MODRINTH_API_BASE = 'https://api.modrinth.com/v2';
|
|
10
|
+
const MODRINTH_VERSION_FILES_ENDPOINT = `${MODRINTH_API_BASE}/version_files`;
|
|
11
|
+
|
|
12
|
+
// Get the workspace root from the current working directory
|
|
13
|
+
const WORKSPACE_ROOT = process.cwd();
|
|
14
|
+
|
|
15
|
+
const DIRECTORIES_TO_SCAN = [
|
|
16
|
+
{ name: 'mods', path: path.join(WORKSPACE_ROOT, 'mods') },
|
|
17
|
+
{ name: 'resourcepacks', path: path.join(WORKSPACE_ROOT, 'resourcepacks') },
|
|
18
|
+
{ name: 'datapacks', path: path.join(WORKSPACE_ROOT, 'datapacks') },
|
|
19
|
+
{ name: 'shaderpacks', path: path.join(WORKSPACE_ROOT, 'shaderpacks') },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculate SHA1 hash of a file
|
|
24
|
+
*/
|
|
25
|
+
async function calculateSHA1(filePath) {
|
|
26
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
27
|
+
return crypto.createHash('sha1').update(fileBuffer).digest('hex');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Find all files in a directory
|
|
32
|
+
*/
|
|
33
|
+
async function findFiles(dirPath) {
|
|
34
|
+
const files = [];
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (entry.isFile() && (entry.name.endsWith('.jar') || entry.name.endsWith('.zip'))) {
|
|
41
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
42
|
+
files.push(fullPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Directory doesn't exist or can't be read - skip it
|
|
47
|
+
if (error.code !== 'ENOENT') {
|
|
48
|
+
console.warn(`Warning: Could not read directory ${dirPath}: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return files;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Scan a directory and return file info with hashes
|
|
57
|
+
*/
|
|
58
|
+
async function scanDirectory(dirInfo) {
|
|
59
|
+
const files = await findFiles(dirInfo.path);
|
|
60
|
+
const fileEntries = [];
|
|
61
|
+
|
|
62
|
+
for (const filePath of files) {
|
|
63
|
+
try {
|
|
64
|
+
const hash = await calculateSHA1(filePath);
|
|
65
|
+
const relativePath = path.relative(WORKSPACE_ROOT, filePath);
|
|
66
|
+
|
|
67
|
+
fileEntries.push({
|
|
68
|
+
path: relativePath,
|
|
69
|
+
fullPath: filePath,
|
|
70
|
+
hash: hash,
|
|
71
|
+
category: dirInfo.name,
|
|
72
|
+
});
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn(`Warning: Could not hash file ${filePath}: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return fileEntries;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Query Modrinth API for version information from hashes
|
|
83
|
+
*/
|
|
84
|
+
async function getVersionsFromHashes(hashes) {
|
|
85
|
+
if (hashes.length === 0) {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch(MODRINTH_VERSION_FILES_ENDPOINT, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({
|
|
96
|
+
hashes: hashes,
|
|
97
|
+
algorithm: 'sha1',
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const errorText = await response.text();
|
|
103
|
+
throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return await response.json();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(`Error querying Modrinth API: ${error.message}`);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Create empty lockfile structure
|
|
116
|
+
*/
|
|
117
|
+
function createEmptyLockfile() {
|
|
118
|
+
return {
|
|
119
|
+
version: LOCKFILE_VERSION,
|
|
120
|
+
generated: new Date().toISOString(),
|
|
121
|
+
dependencies: {},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create lockfile structure from file info and version data
|
|
127
|
+
*/
|
|
128
|
+
function createLockfile(fileEntries, versionData) {
|
|
129
|
+
const lockfile = createEmptyLockfile();
|
|
130
|
+
|
|
131
|
+
// Organize by category
|
|
132
|
+
for (const fileInfo of fileEntries) {
|
|
133
|
+
const version = versionData[fileInfo.hash];
|
|
134
|
+
|
|
135
|
+
lockfile.dependencies[fileInfo.category] ||= [];
|
|
136
|
+
|
|
137
|
+
const entry = {
|
|
138
|
+
path: fileInfo.path,
|
|
139
|
+
version: version || null,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (!version) {
|
|
143
|
+
console.warn(`Warning: File ${fileInfo.path} not found on Modrinth`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
lockfile.dependencies[fileInfo.category].push(entry);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return lockfile;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Write lockfile to disk
|
|
154
|
+
*/
|
|
155
|
+
async function writeLockfile(lockfile, outputPath) {
|
|
156
|
+
const content = JSON.stringify(lockfile, null, 2);
|
|
157
|
+
await fs.writeFile(outputPath, content, 'utf-8');
|
|
158
|
+
console.log(`Lockfile written to: ${outputPath}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Main execution function
|
|
163
|
+
*/
|
|
164
|
+
async function main() {
|
|
165
|
+
console.log('Scanning directories for modpack files...');
|
|
166
|
+
|
|
167
|
+
// Scan all directories
|
|
168
|
+
const allFileEntries = [];
|
|
169
|
+
for (const dirInfo of DIRECTORIES_TO_SCAN) {
|
|
170
|
+
console.log(`Scanning ${dirInfo.name}...`);
|
|
171
|
+
const fileEntries = await scanDirectory(dirInfo);
|
|
172
|
+
console.log(` Found ${fileEntries.length} file(s)`);
|
|
173
|
+
allFileEntries.push(...fileEntries);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (allFileEntries.length === 0) {
|
|
177
|
+
console.log('No files found. Creating empty lockfile.');
|
|
178
|
+
const outputPath = path.join(WORKSPACE_ROOT, MODPACK_LOCKFILE_NAME);
|
|
179
|
+
await writeLockfile(createEmptyLockfile(), outputPath);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`\nTotal files found: ${allFileEntries.length}`);
|
|
184
|
+
console.log('\nQuerying Modrinth API...');
|
|
185
|
+
|
|
186
|
+
// Extract all hashes
|
|
187
|
+
const hashes = allFileEntries.map(info => info.hash);
|
|
188
|
+
|
|
189
|
+
// Query Modrinth API
|
|
190
|
+
const versionData = await getVersionsFromHashes(hashes);
|
|
191
|
+
|
|
192
|
+
console.log(`\nFound version information for ${Object.keys(versionData).length} out of ${hashes.length} files`);
|
|
193
|
+
|
|
194
|
+
// Create lockfile
|
|
195
|
+
const lockfile = createLockfile(allFileEntries, versionData);
|
|
196
|
+
|
|
197
|
+
// Write lockfile
|
|
198
|
+
const outputPath = path.join(WORKSPACE_ROOT, MODPACK_LOCKFILE_NAME);
|
|
199
|
+
await writeLockfile(lockfile, outputPath);
|
|
200
|
+
|
|
201
|
+
// Summary
|
|
202
|
+
console.log('\n=== Summary ===');
|
|
203
|
+
for (const [category, entries] of Object.entries(lockfile.dependencies)) {
|
|
204
|
+
const withVersion = entries.filter(e => e.version !== null).length;
|
|
205
|
+
const withoutVersion = entries.length - withVersion;
|
|
206
|
+
console.log(`${category}: ${entries.length} file(s) (${withVersion} found on Modrinth, ${withoutVersion} unknown)`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
main().catch(error => {
|
|
211
|
+
console.error('Error:', error);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "modpack-lock",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a modpack lockfile for files hosted on Modrinth (mods, resource packs, shaders and datapacks)",
|
|
5
|
+
"homepage": "https://github.com/nickesc/modpack-lock#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/nickesc/modpack-lock/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/nickesc/modpack-lock.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "N. Escobar <nick@nescobar.media> (https://nickesc.github.io/)",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"bin": {
|
|
18
|
+
"modpack-lock": "index.js"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"modrinth",
|
|
22
|
+
"minecraft",
|
|
23
|
+
"modpack",
|
|
24
|
+
"lockfile"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node index.js",
|
|
31
|
+
"start": "node index.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.js",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"package.json"
|
|
38
|
+
]
|
|
39
|
+
}
|