modpack-lock 0.1.2 → 0.2.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/README.md +19 -10
- package/package.json +16 -8
- package/src/generate-lockfile.js +517 -0
- package/src/modpack-lock.js +32 -0
- package/index.js +0 -315
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
1
|
<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>
|
|
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://github.com/nickesc/modpack-lock/actions/workflows/generateLockfile-tests.yml"><img alt="Tests: github.com/nickesc/modpack-lock/actions/workflows/generateLockfile-tests.yml" src="https://img.shields.io/github/actions/workflow/status/nickesc/modpack-lock/generateLockfile-tests.yml?logo=github&label=tests&logoColor=white&style=for-the-badge&labelColor=%23505050"></a>
|
|
4
4
|
|
|
5
5
|
# modpack-lock
|
|
6
6
|
|
|
@@ -37,16 +37,25 @@ npx modpack-lock
|
|
|
37
37
|
|
|
38
38
|
Navigate to your Minecraft profile directory (the folder containing `mods`, `resourcepacks`, `datapacks`, and `shaderpacks` folders) and run:
|
|
39
39
|
|
|
40
|
-
```
|
|
41
|
-
modpack-lock
|
|
42
|
-
|
|
40
|
+
```text
|
|
41
|
+
Usage: modpack-lock [options]
|
|
42
|
+
|
|
43
|
+
Create a modpack lockfile for files hosted on Modrinth (mods, resource packs, shaders and datapacks)
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
Options:
|
|
46
|
+
-d, --dry-run Dry-run mode - no files will be written
|
|
47
|
+
-g, --gitignore Print .gitignore rules for files not hosted on Modrinth
|
|
48
|
+
-r, --readme Generate README.md files for each category
|
|
49
|
+
-p, --path <path> Path to the modpack directory
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
|
|
51
|
+
LOGGING
|
|
52
|
+
-q, --quiet Quiet mode - only show errors and warnings
|
|
53
|
+
-s, --silent Silent mode - no output
|
|
54
|
+
|
|
55
|
+
INFORMATION
|
|
56
|
+
-V, --version output the version number
|
|
57
|
+
--help display help for modpack-lock
|
|
58
|
+
```
|
|
50
59
|
|
|
51
60
|
The script will:
|
|
52
61
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modpack-lock",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Create a modpack lockfile for files hosted on Modrinth (mods, resource packs, shaders and datapacks)",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/nickesc/modpack-lock/issues"
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"author": "N. Escobar <nick@nescobar.media> (https://nickesc.github.io/)",
|
|
14
14
|
"type": "module",
|
|
15
|
-
"main": "
|
|
15
|
+
"main": "src/generate-lockfile.js",
|
|
16
16
|
"bin": {
|
|
17
|
-
"modpack-lock": "
|
|
17
|
+
"modpack-lock": "src/modpack-lock.js"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"modrinth",
|
|
@@ -26,13 +26,21 @@
|
|
|
26
26
|
"node": ">=18.0.0"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"test": "
|
|
30
|
-
"start": "node
|
|
29
|
+
"test": "vitest --run",
|
|
30
|
+
"start": "node src/modpack-lock.js",
|
|
31
|
+
"modpack-lock": "node src/modpack-lock.js"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
33
|
-
"index.js",
|
|
34
34
|
"README.md",
|
|
35
35
|
"LICENSE",
|
|
36
|
-
"package.json"
|
|
37
|
-
|
|
36
|
+
"package.json",
|
|
37
|
+
"src/*.js"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"commander": "^14.0.2"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"unzipper": "^0.12.3",
|
|
44
|
+
"vitest": "^4.0.16"
|
|
45
|
+
}
|
|
38
46
|
}
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const LOCKFILE_VERSION = '1.0.1';
|
|
6
|
+
const MODPACK_LOCKFILE_NAME = 'modpack.lock';
|
|
7
|
+
const MODRINTH_API_BASE = 'https://api.modrinth.com/v2';
|
|
8
|
+
const MODRINTH_VERSION_FILES_ENDPOINT = `${MODRINTH_API_BASE}/version_files`;
|
|
9
|
+
const MODRINTH_PROJECTS_ENDPOINT = `${MODRINTH_API_BASE}/projects`;
|
|
10
|
+
const MODRINTH_USERS_ENDPOINT = `${MODRINTH_API_BASE}/users`;
|
|
11
|
+
const BATCH_SIZE = 100;
|
|
12
|
+
|
|
13
|
+
// Get the workspace root from the current working directory
|
|
14
|
+
//const WORKSPACE_ROOT = process.cwd();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a logger function that respects quiet mode
|
|
18
|
+
*/
|
|
19
|
+
function createLogger(quiet) {
|
|
20
|
+
if (quiet) {
|
|
21
|
+
return () => {};
|
|
22
|
+
}
|
|
23
|
+
return (...args) => console.log(...args);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Silence all console.log output
|
|
28
|
+
*/
|
|
29
|
+
function silenceConsole() {
|
|
30
|
+
console.log = () => {};
|
|
31
|
+
console.warn = () => {};
|
|
32
|
+
console.error = () => {};
|
|
33
|
+
console.info = () => {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the directories to scan for modpack files
|
|
38
|
+
*/
|
|
39
|
+
function getScanDirectories(directoryPath) {
|
|
40
|
+
return [
|
|
41
|
+
{ name: 'mods', path: path.join(directoryPath, 'mods') },
|
|
42
|
+
{ name: 'resourcepacks', path: path.join(directoryPath, 'resourcepacks') },
|
|
43
|
+
{ name: 'datapacks', path: path.join(directoryPath, 'datapacks') },
|
|
44
|
+
{ name: 'shaderpacks', path: path.join(directoryPath, 'shaderpacks') },
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate SHA1 hash of a file
|
|
50
|
+
*/
|
|
51
|
+
async function calculateSHA1(filePath) {
|
|
52
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
53
|
+
return crypto.createHash('sha1').update(fileBuffer).digest('hex');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Find all files in a directory
|
|
58
|
+
*/
|
|
59
|
+
async function findFiles(dirPath) {
|
|
60
|
+
const files = [];
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
64
|
+
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.isFile() && (entry.name.endsWith('.jar') || entry.name.endsWith('.zip'))) {
|
|
67
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
68
|
+
files.push(fullPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error.code !== 'ENOENT') {
|
|
73
|
+
console.warn(`Warning: Could not read directory ${dirPath}: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Scan a directory and return file info with hashes
|
|
82
|
+
*/
|
|
83
|
+
async function scanDirectory(dirInfo, workspaceRoot) {
|
|
84
|
+
const files = await findFiles(dirInfo.path);
|
|
85
|
+
const fileEntries = [];
|
|
86
|
+
|
|
87
|
+
for (const filePath of files) {
|
|
88
|
+
try {
|
|
89
|
+
const hash = await calculateSHA1(filePath);
|
|
90
|
+
const relativePath = path.relative(workspaceRoot, filePath);
|
|
91
|
+
|
|
92
|
+
fileEntries.push({
|
|
93
|
+
path: relativePath,
|
|
94
|
+
fullPath: filePath,
|
|
95
|
+
hash: hash,
|
|
96
|
+
category: dirInfo.name,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn(`Warning: Could not hash file ${filePath}: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return fileEntries;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Query Modrinth API for version information from hashes
|
|
108
|
+
*/
|
|
109
|
+
async function getVersionsFromHashes(hashes) {
|
|
110
|
+
if (hashes.length === 0) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(MODRINTH_VERSION_FILES_ENDPOINT, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
},
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
hashes: hashes,
|
|
122
|
+
algorithm: 'sha1',
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
const errorText = await response.text();
|
|
128
|
+
throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return await response.json();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(`Error querying Modrinth API: ${error.message}`);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Split an array into chunks of specified size
|
|
140
|
+
*/
|
|
141
|
+
function chunkArray(array, size) {
|
|
142
|
+
const chunks = [];
|
|
143
|
+
for (let i = 0; i < array.length; i += size) {
|
|
144
|
+
chunks.push(array.slice(i, i + size));
|
|
145
|
+
}
|
|
146
|
+
return chunks;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fetch multiple projects by their IDs in batches
|
|
151
|
+
*/
|
|
152
|
+
async function getProjects(projectIds) {
|
|
153
|
+
if (projectIds.length === 0) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const chunks = chunkArray(projectIds, BATCH_SIZE);
|
|
158
|
+
const results = [];
|
|
159
|
+
|
|
160
|
+
for (const chunk of chunks) {
|
|
161
|
+
try {
|
|
162
|
+
const url = `${MODRINTH_PROJECTS_ENDPOINT}?ids=${encodeURIComponent(JSON.stringify(chunk))}`;
|
|
163
|
+
const response = await fetch(url);
|
|
164
|
+
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
const errorText = await response.text();
|
|
167
|
+
throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const data = await response.json();
|
|
171
|
+
results.push(...data);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(`Error fetching projects: ${error.message}`);
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fetch multiple users by their IDs in batches
|
|
183
|
+
*/
|
|
184
|
+
async function getUsers(userIds) {
|
|
185
|
+
if (userIds.length === 0) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const chunks = chunkArray(userIds, BATCH_SIZE);
|
|
190
|
+
const results = [];
|
|
191
|
+
|
|
192
|
+
for (const chunk of chunks) {
|
|
193
|
+
try {
|
|
194
|
+
const url = `${MODRINTH_USERS_ENDPOINT}?ids=${encodeURIComponent(JSON.stringify(chunk))}`;
|
|
195
|
+
const response = await fetch(url);
|
|
196
|
+
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
const errorText = await response.text();
|
|
199
|
+
throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const data = await response.json();
|
|
203
|
+
results.push(...data);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error(`Error fetching users: ${error.message}`);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return results;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create empty lockfile structure
|
|
216
|
+
*/
|
|
217
|
+
function createEmptyLockfile() {
|
|
218
|
+
return {
|
|
219
|
+
version: LOCKFILE_VERSION,
|
|
220
|
+
generated: new Date().toISOString(),
|
|
221
|
+
total: 0,
|
|
222
|
+
counts: {},
|
|
223
|
+
dependencies: {},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Create lockfile structure from file info and version data
|
|
229
|
+
*/
|
|
230
|
+
function createLockfile(fileEntries, versionData) {
|
|
231
|
+
const lockfile = createEmptyLockfile();
|
|
232
|
+
|
|
233
|
+
// Organize by category
|
|
234
|
+
for (const fileInfo of fileEntries) {
|
|
235
|
+
const version = versionData[fileInfo.hash];
|
|
236
|
+
|
|
237
|
+
lockfile.dependencies[fileInfo.category] ||= [];
|
|
238
|
+
|
|
239
|
+
const entry = {
|
|
240
|
+
path: fileInfo.path,
|
|
241
|
+
version: version || null,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (!version) {
|
|
245
|
+
console.warn(`Warning: File ${fileInfo.path} not found on Modrinth`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lockfile.dependencies[fileInfo.category].push(entry);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Calculate counts for each category
|
|
252
|
+
for (const [category, entries] of Object.entries(lockfile.dependencies)) {
|
|
253
|
+
lockfile.counts[category] = entries.length;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
lockfile.total = fileEntries.length;
|
|
257
|
+
|
|
258
|
+
return lockfile;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Write lockfile to disk
|
|
263
|
+
*/
|
|
264
|
+
async function writeLockfile(lockfile, outputPath, log) {
|
|
265
|
+
const content = JSON.stringify(lockfile, null, 2);
|
|
266
|
+
await fs.writeFile(outputPath, content, 'utf-8');
|
|
267
|
+
log(`Lockfile written to: ${outputPath}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Generate README.md content for a category
|
|
272
|
+
*/
|
|
273
|
+
function generateCategoryReadme(category, entries, projectsMap, usersMap) {
|
|
274
|
+
const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1);
|
|
275
|
+
const lines = [`# ${categoryTitle}`, '', '| Name | Author | Version |', '|-|-|-|'];
|
|
276
|
+
|
|
277
|
+
// Map category to Modrinth URL path segment
|
|
278
|
+
const categoryPathMap = {
|
|
279
|
+
mods: 'mod',
|
|
280
|
+
resourcepacks: 'resourcepack',
|
|
281
|
+
shaderpacks: 'shader',
|
|
282
|
+
datapacks: 'datapack',
|
|
283
|
+
};
|
|
284
|
+
const categoryPath = categoryPathMap[category] || 'project';
|
|
285
|
+
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
const version = entry.version;
|
|
288
|
+
let nameCell = '';
|
|
289
|
+
let authorCell = '';
|
|
290
|
+
let versionCell = '';
|
|
291
|
+
|
|
292
|
+
if (version && version.project_id) {
|
|
293
|
+
const project = projectsMap[version.project_id];
|
|
294
|
+
const author = version.author_id ? usersMap[version.author_id] : null;
|
|
295
|
+
|
|
296
|
+
// Name column with icon and link
|
|
297
|
+
if (project) {
|
|
298
|
+
const projectName = project.title || project.slug || 'Unknown';
|
|
299
|
+
const projectSlug = project.slug || project.id;
|
|
300
|
+
const projectUrl = `https://modrinth.com/${categoryPath}/${projectSlug}`;
|
|
301
|
+
|
|
302
|
+
if (project.icon_url) {
|
|
303
|
+
nameCell = `<img alt="Icon" src="${project.icon_url}" height="20px"> [${projectName}](${projectUrl})`;
|
|
304
|
+
} else {
|
|
305
|
+
nameCell = `[${projectName}](${projectUrl})`;
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
// Project not found, use filename
|
|
309
|
+
const fileName = path.basename(entry.path);
|
|
310
|
+
nameCell = fileName;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Author column with avatar and link
|
|
314
|
+
if (author) {
|
|
315
|
+
const authorName = author.username || 'Unknown';
|
|
316
|
+
const authorUrl = `https://modrinth.com/user/${authorName}`;
|
|
317
|
+
|
|
318
|
+
if (author.avatar_url) {
|
|
319
|
+
authorCell = `<img alt="Avatar" src="${author.avatar_url}" height="20px"> [${authorName}](${authorUrl})`;
|
|
320
|
+
} else {
|
|
321
|
+
authorCell = `[${authorName}](${authorUrl})`;
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
authorCell = 'Unknown';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Version column
|
|
328
|
+
versionCell = version.version_number || 'Unknown';
|
|
329
|
+
} else {
|
|
330
|
+
// File not found on Modrinth
|
|
331
|
+
const fileName = path.basename(entry.path);
|
|
332
|
+
nameCell = fileName;
|
|
333
|
+
authorCell = 'Unknown';
|
|
334
|
+
versionCell = '-';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
lines.push(`| ${nameCell} | ${authorCell} | ${versionCell} |`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return lines.join('\n') + '\n';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Generate .gitignore rules for files not hosted on Modrinth
|
|
345
|
+
*/
|
|
346
|
+
function generateGitignoreRules(lockfile) {
|
|
347
|
+
const rules = [];
|
|
348
|
+
const exceptions = [];
|
|
349
|
+
|
|
350
|
+
// Base ignore patterns for each category
|
|
351
|
+
rules.push('mods/*.jar');
|
|
352
|
+
rules.push('resourcepacks/*.zip');
|
|
353
|
+
rules.push('datapacks/*.zip');
|
|
354
|
+
rules.push('shaderpacks/*.zip');
|
|
355
|
+
rules.push('');
|
|
356
|
+
rules.push('## Exceptions');
|
|
357
|
+
|
|
358
|
+
// Find files not hosted on Modrinth
|
|
359
|
+
for (const [category, entries] of Object.entries(lockfile.dependencies)) {
|
|
360
|
+
for (const entry of entries) {
|
|
361
|
+
if (entry.version === null) {
|
|
362
|
+
exceptions.push(`!${entry.path}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Add exceptions if any
|
|
368
|
+
if (exceptions.length > 0) {
|
|
369
|
+
rules.push(...exceptions);
|
|
370
|
+
} else {
|
|
371
|
+
rules.push('# No exceptions needed - all files are hosted on Modrinth');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return rules.join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Main execution function
|
|
379
|
+
*/
|
|
380
|
+
async function generateLockfile(config) {
|
|
381
|
+
const log = createLogger(config.quiet);
|
|
382
|
+
|
|
383
|
+
if (config.silent) {
|
|
384
|
+
silenceConsole();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (config.dryRun) {
|
|
388
|
+
log('[DRY RUN] Preview mode - no files will be written');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
log('Scanning directories for modpack files...');
|
|
392
|
+
|
|
393
|
+
// Scan all directories
|
|
394
|
+
const allFileEntries = [];
|
|
395
|
+
for (const dirInfo of getScanDirectories(config.path)) {
|
|
396
|
+
log(`Scanning ${dirInfo.name}...`);
|
|
397
|
+
const fileEntries = await scanDirectory(dirInfo, config.path);
|
|
398
|
+
log(` Found ${fileEntries.length} file(s)`);
|
|
399
|
+
allFileEntries.push(...fileEntries);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (allFileEntries.length === 0) {
|
|
403
|
+
log('No files found. Creating empty lockfile.');
|
|
404
|
+
const outputPath = path.join(config.path, MODPACK_LOCKFILE_NAME);
|
|
405
|
+
if (config.dryRun) {
|
|
406
|
+
log(`[DRY RUN] Would write lockfile to: ${outputPath}`);
|
|
407
|
+
} else {
|
|
408
|
+
await writeLockfile(createEmptyLockfile(), outputPath, log);
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
log(`\nTotal files found: ${allFileEntries.length}`);
|
|
414
|
+
log('\nQuerying Modrinth API...');
|
|
415
|
+
|
|
416
|
+
// Extract all hashes
|
|
417
|
+
const hashes = allFileEntries.map(info => info.hash);
|
|
418
|
+
|
|
419
|
+
// Query Modrinth API
|
|
420
|
+
const versionData = await getVersionsFromHashes(hashes);
|
|
421
|
+
|
|
422
|
+
log(`\nFound version information for ${Object.keys(versionData).length} out of ${hashes.length} files`);
|
|
423
|
+
|
|
424
|
+
// Create lockfile
|
|
425
|
+
const lockfile = createLockfile(allFileEntries, versionData);
|
|
426
|
+
|
|
427
|
+
// Write lockfile
|
|
428
|
+
const outputPath = path.join(config.path, MODPACK_LOCKFILE_NAME);
|
|
429
|
+
if (config.dryRun) {
|
|
430
|
+
log(`[DRY RUN] Would write lockfile to: ${outputPath}`);
|
|
431
|
+
} else {
|
|
432
|
+
await writeLockfile(lockfile, outputPath, log);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Summary
|
|
436
|
+
log('\n=== Summary ===');
|
|
437
|
+
for (const [category, entries] of Object.entries(lockfile.dependencies)) {
|
|
438
|
+
const withVersion = entries.filter(e => e.version !== null).length;
|
|
439
|
+
const withoutVersion = entries.length - withVersion;
|
|
440
|
+
log(`${category}: ${entries.length} file(s) (${withVersion} found on Modrinth, ${withoutVersion} unknown)`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Generate .gitignore rules
|
|
444
|
+
if (config.gitignore) {
|
|
445
|
+
log('\n=== .gitignore Rules ===');
|
|
446
|
+
log(generateGitignoreRules(lockfile));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Generate README files
|
|
450
|
+
if (config.readme) {
|
|
451
|
+
log('\nGenerating README files...');
|
|
452
|
+
|
|
453
|
+
// Collect unique project IDs and author IDs from version data
|
|
454
|
+
const projectIds = new Set();
|
|
455
|
+
const authorIds = new Set();
|
|
456
|
+
|
|
457
|
+
for (const [category, entries] of Object.entries(lockfile.dependencies)) {
|
|
458
|
+
for (const entry of entries) {
|
|
459
|
+
if (entry.version && entry.version.project_id) {
|
|
460
|
+
projectIds.add(entry.version.project_id);
|
|
461
|
+
}
|
|
462
|
+
if (entry.version && entry.version.author_id) {
|
|
463
|
+
authorIds.add(entry.version.author_id);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Fetch projects and users in parallel
|
|
469
|
+
log(`Fetching data for ${projectIds.size} project(s) and ${authorIds.size} user(s)...`);
|
|
470
|
+
|
|
471
|
+
const [projects, users] = await Promise.all([
|
|
472
|
+
getProjects(Array.from(projectIds)),
|
|
473
|
+
getUsers(Array.from(authorIds)),
|
|
474
|
+
]);
|
|
475
|
+
|
|
476
|
+
// Map projects and users to their IDs
|
|
477
|
+
const projectsMap = {};
|
|
478
|
+
for (const project of projects) {
|
|
479
|
+
projectsMap[project.id] = project;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const usersMap = {};
|
|
483
|
+
for (const user of users) {
|
|
484
|
+
usersMap[user.id] = user;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Generate README for each category
|
|
488
|
+
for (const [category, entries] of Object.entries(lockfile.dependencies)) {
|
|
489
|
+
if (entries.length === 0) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const readmeContent = generateCategoryReadme(category, entries, projectsMap, usersMap);
|
|
494
|
+
const categoryDir = getScanDirectories(config.path).find(d => d.name === category);
|
|
495
|
+
|
|
496
|
+
if (categoryDir) {
|
|
497
|
+
const readmePath = path.join(categoryDir.path, 'README.md');
|
|
498
|
+
|
|
499
|
+
if (config.dryRun) {
|
|
500
|
+
log(`[DRY RUN] Would write README to: ${readmePath}`);
|
|
501
|
+
} else {
|
|
502
|
+
try {
|
|
503
|
+
await fs.writeFile(readmePath, readmeContent, 'utf-8');
|
|
504
|
+
log(`Generated README: ${readmePath}`);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
console.warn(`Warning: Could not write README to ${readmePath}: ${error.message}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
log('README generation complete.');
|
|
513
|
+
}
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export default generateLockfile;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env NODE_OPTIONS=--no-warnings node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import generateLockfile from './generate-lockfile.js';
|
|
5
|
+
|
|
6
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
7
|
+
const modpackLock = new Command('modpack-lock');
|
|
8
|
+
|
|
9
|
+
modpackLock
|
|
10
|
+
.name(pkg.name)
|
|
11
|
+
.description(pkg.description)
|
|
12
|
+
.summary("Create a modpack lockfile")
|
|
13
|
+
.optionsGroup("Options:")
|
|
14
|
+
.option('-d, --dry-run', 'Dry-run mode - no files will be written')
|
|
15
|
+
.option('-g, --gitignore', 'Print .gitignore rules for files not hosted on Modrinth')
|
|
16
|
+
.option('-r, --readme', 'Generate README.md files for each category')
|
|
17
|
+
.option('-p, --path <path>', 'Path to the modpack directory')
|
|
18
|
+
.optionsGroup("LOGGING")
|
|
19
|
+
.option('-q, --quiet', 'Quiet mode - only show errors and warnings')
|
|
20
|
+
.option('-s, --silent', 'Silent mode - no output')
|
|
21
|
+
.optionsGroup("INFORMATION")
|
|
22
|
+
.helpOption("--help", `display help for ${pkg.name}`)
|
|
23
|
+
.version(pkg.version)
|
|
24
|
+
.action((options) => {
|
|
25
|
+
generateLockfile({ ...options, path: options.path || process.cwd() }).catch(error => {
|
|
26
|
+
console.error('Error:', error);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
modpackLock.parse()
|
|
32
|
+
|
package/index.js
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
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.1';
|
|
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
|
-
/**
|
|
16
|
-
* Parse command-line arguments
|
|
17
|
-
*/
|
|
18
|
-
function parseArgs() {
|
|
19
|
-
const args = process.argv.slice(2);
|
|
20
|
-
return {
|
|
21
|
-
dryRun: args.includes('--dry-run') || args.includes('-d'),
|
|
22
|
-
quiet: args.includes('--quiet') || args.includes('-q'),
|
|
23
|
-
silent: args.includes('--silent') || args.includes('-s'),
|
|
24
|
-
gitignore: args.includes('--gitignore') || args.includes('-g'),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Create a logger function that respects quiet mode
|
|
30
|
-
*/
|
|
31
|
-
function createLogger(quiet) {
|
|
32
|
-
if (quiet) {
|
|
33
|
-
return () => {}; // No-op function when quiet
|
|
34
|
-
}
|
|
35
|
-
return (...args) => console.log(...args);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Silence all console.log output
|
|
40
|
-
*/
|
|
41
|
-
function silenceConsole() {
|
|
42
|
-
console.log = () => {};
|
|
43
|
-
console.warn = () => {};
|
|
44
|
-
console.error = () => {};
|
|
45
|
-
console.info = () => {};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const DIRECTORIES_TO_SCAN = [
|
|
49
|
-
{ name: 'mods', path: path.join(WORKSPACE_ROOT, 'mods') },
|
|
50
|
-
{ name: 'resourcepacks', path: path.join(WORKSPACE_ROOT, 'resourcepacks') },
|
|
51
|
-
{ name: 'datapacks', path: path.join(WORKSPACE_ROOT, 'datapacks') },
|
|
52
|
-
{ name: 'shaderpacks', path: path.join(WORKSPACE_ROOT, 'shaderpacks') },
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Calculate SHA1 hash of a file
|
|
57
|
-
*/
|
|
58
|
-
async function calculateSHA1(filePath) {
|
|
59
|
-
const fileBuffer = await fs.readFile(filePath);
|
|
60
|
-
return crypto.createHash('sha1').update(fileBuffer).digest('hex');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Find all files in a directory
|
|
65
|
-
*/
|
|
66
|
-
async function findFiles(dirPath) {
|
|
67
|
-
const files = [];
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
71
|
-
|
|
72
|
-
for (const entry of entries) {
|
|
73
|
-
if (entry.isFile() && (entry.name.endsWith('.jar') || entry.name.endsWith('.zip'))) {
|
|
74
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
75
|
-
files.push(fullPath);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
} catch (error) {
|
|
79
|
-
// Directory doesn't exist or can't be read - skip it
|
|
80
|
-
if (error.code !== 'ENOENT') {
|
|
81
|
-
console.warn(`Warning: Could not read directory ${dirPath}: ${error.message}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return files;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Scan a directory and return file info with hashes
|
|
90
|
-
*/
|
|
91
|
-
async function scanDirectory(dirInfo) {
|
|
92
|
-
const files = await findFiles(dirInfo.path);
|
|
93
|
-
const fileEntries = [];
|
|
94
|
-
|
|
95
|
-
for (const filePath of files) {
|
|
96
|
-
try {
|
|
97
|
-
const hash = await calculateSHA1(filePath);
|
|
98
|
-
const relativePath = path.relative(WORKSPACE_ROOT, filePath);
|
|
99
|
-
|
|
100
|
-
fileEntries.push({
|
|
101
|
-
path: relativePath,
|
|
102
|
-
fullPath: filePath,
|
|
103
|
-
hash: hash,
|
|
104
|
-
category: dirInfo.name,
|
|
105
|
-
});
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.warn(`Warning: Could not hash file ${filePath}: ${error.message}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return fileEntries;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Query Modrinth API for version information from hashes
|
|
116
|
-
*/
|
|
117
|
-
async function getVersionsFromHashes(hashes) {
|
|
118
|
-
if (hashes.length === 0) {
|
|
119
|
-
return {};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const response = await fetch(MODRINTH_VERSION_FILES_ENDPOINT, {
|
|
124
|
-
method: 'POST',
|
|
125
|
-
headers: {
|
|
126
|
-
'Content-Type': 'application/json',
|
|
127
|
-
},
|
|
128
|
-
body: JSON.stringify({
|
|
129
|
-
hashes: hashes,
|
|
130
|
-
algorithm: 'sha1',
|
|
131
|
-
}),
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
if (!response.ok) {
|
|
135
|
-
const errorText = await response.text();
|
|
136
|
-
throw new Error(`Modrinth API error (${response.status}): ${errorText}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return await response.json();
|
|
140
|
-
} catch (error) {
|
|
141
|
-
console.error(`Error querying Modrinth API: ${error.message}`);
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Create empty lockfile structure
|
|
149
|
-
*/
|
|
150
|
-
function createEmptyLockfile() {
|
|
151
|
-
return {
|
|
152
|
-
version: LOCKFILE_VERSION,
|
|
153
|
-
generated: new Date().toISOString(),
|
|
154
|
-
total: 0,
|
|
155
|
-
counts: {},
|
|
156
|
-
dependencies: {},
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Create lockfile structure from file info and version data
|
|
162
|
-
*/
|
|
163
|
-
function createLockfile(fileEntries, versionData) {
|
|
164
|
-
const lockfile = createEmptyLockfile();
|
|
165
|
-
|
|
166
|
-
// Organize by category
|
|
167
|
-
for (const fileInfo of fileEntries) {
|
|
168
|
-
const version = versionData[fileInfo.hash];
|
|
169
|
-
|
|
170
|
-
lockfile.dependencies[fileInfo.category] ||= [];
|
|
171
|
-
|
|
172
|
-
const entry = {
|
|
173
|
-
path: fileInfo.path,
|
|
174
|
-
version: version || null,
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
if (!version) {
|
|
178
|
-
console.warn(`Warning: File ${fileInfo.path} not found on Modrinth`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
lockfile.dependencies[fileInfo.category].push(entry);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Calculate counts for each category
|
|
185
|
-
for (const [category, entries] of Object.entries(lockfile.dependencies)) {
|
|
186
|
-
lockfile.counts[category] = entries.length;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Calculate total count
|
|
190
|
-
lockfile.total = fileEntries.length;
|
|
191
|
-
|
|
192
|
-
return lockfile;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Write lockfile to disk
|
|
197
|
-
*/
|
|
198
|
-
async function writeLockfile(lockfile, outputPath, log) {
|
|
199
|
-
const content = JSON.stringify(lockfile, null, 2);
|
|
200
|
-
await fs.writeFile(outputPath, content, 'utf-8');
|
|
201
|
-
log(`Lockfile written to: ${outputPath}`);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Generate .gitignore rules for files not hosted on Modrinth
|
|
206
|
-
*/
|
|
207
|
-
function generateGitignoreRules(lockfile) {
|
|
208
|
-
const rules = [];
|
|
209
|
-
const exceptions = [];
|
|
210
|
-
|
|
211
|
-
// Base ignore patterns for each category
|
|
212
|
-
rules.push('mods/*.jar');
|
|
213
|
-
rules.push('resourcepacks/*.zip');
|
|
214
|
-
rules.push('datapacks/*.zip');
|
|
215
|
-
rules.push('shaderpacks/*.zip');
|
|
216
|
-
rules.push('');
|
|
217
|
-
rules.push('## Exceptions');
|
|
218
|
-
|
|
219
|
-
// Find files not hosted on Modrinth
|
|
220
|
-
for (const [category, entries] of Object.entries(lockfile.dependencies)) {
|
|
221
|
-
for (const entry of entries) {
|
|
222
|
-
if (entry.version === null) {
|
|
223
|
-
exceptions.push(`!${entry.path}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Add exceptions if any
|
|
229
|
-
if (exceptions.length > 0) {
|
|
230
|
-
rules.push(...exceptions);
|
|
231
|
-
} else {
|
|
232
|
-
rules.push('# No exceptions needed - all files are hosted on Modrinth');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return rules.join('\n');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Main execution function
|
|
240
|
-
*/
|
|
241
|
-
async function main() {
|
|
242
|
-
const config = parseArgs();
|
|
243
|
-
const log = createLogger(config.quiet);
|
|
244
|
-
|
|
245
|
-
if (config.silent) {
|
|
246
|
-
silenceConsole();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (config.dryRun) {
|
|
250
|
-
log('[DRY RUN] Preview mode - no files will be written');
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
log('Scanning directories for modpack files...');
|
|
254
|
-
|
|
255
|
-
// Scan all directories
|
|
256
|
-
const allFileEntries = [];
|
|
257
|
-
for (const dirInfo of DIRECTORIES_TO_SCAN) {
|
|
258
|
-
log(`Scanning ${dirInfo.name}...`);
|
|
259
|
-
const fileEntries = await scanDirectory(dirInfo);
|
|
260
|
-
log(` Found ${fileEntries.length} file(s)`);
|
|
261
|
-
allFileEntries.push(...fileEntries);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (allFileEntries.length === 0) {
|
|
265
|
-
log('No files found. Creating empty lockfile.');
|
|
266
|
-
const outputPath = path.join(WORKSPACE_ROOT, MODPACK_LOCKFILE_NAME);
|
|
267
|
-
if (config.dryRun) {
|
|
268
|
-
log(`[DRY RUN] Would write lockfile to: ${outputPath}`);
|
|
269
|
-
} else {
|
|
270
|
-
await writeLockfile(createEmptyLockfile(), outputPath, log);
|
|
271
|
-
}
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
log(`\nTotal files found: ${allFileEntries.length}`);
|
|
276
|
-
log('\nQuerying Modrinth API...');
|
|
277
|
-
|
|
278
|
-
// Extract all hashes
|
|
279
|
-
const hashes = allFileEntries.map(info => info.hash);
|
|
280
|
-
|
|
281
|
-
// Query Modrinth API
|
|
282
|
-
const versionData = await getVersionsFromHashes(hashes);
|
|
283
|
-
|
|
284
|
-
log(`\nFound version information for ${Object.keys(versionData).length} out of ${hashes.length} files`);
|
|
285
|
-
|
|
286
|
-
// Create lockfile
|
|
287
|
-
const lockfile = createLockfile(allFileEntries, versionData);
|
|
288
|
-
|
|
289
|
-
// Write lockfile
|
|
290
|
-
const outputPath = path.join(WORKSPACE_ROOT, MODPACK_LOCKFILE_NAME);
|
|
291
|
-
if (config.dryRun) {
|
|
292
|
-
log(`[DRY RUN] Would write lockfile to: ${outputPath}`);
|
|
293
|
-
} else {
|
|
294
|
-
await writeLockfile(lockfile, outputPath, log);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Summary
|
|
298
|
-
log('\n=== Summary ===');
|
|
299
|
-
for (const [category, entries] of Object.entries(lockfile.dependencies)) {
|
|
300
|
-
const withVersion = entries.filter(e => e.version !== null).length;
|
|
301
|
-
const withoutVersion = entries.length - withVersion;
|
|
302
|
-
log(`${category}: ${entries.length} file(s) (${withVersion} found on Modrinth, ${withoutVersion} unknown)`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Generate .gitignore rules if requested
|
|
306
|
-
if (config.gitignore) {
|
|
307
|
-
log('\n=== .gitignore Rules ===');
|
|
308
|
-
log(generateGitignoreRules(lockfile));
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
main().catch(error => {
|
|
313
|
-
console.error('Error:', error);
|
|
314
|
-
process.exit(1);
|
|
315
|
-
});
|