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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/index.js +213 -0
  4. 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
+ }