modpack-lock 0.2.0 → 0.3.1

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 CHANGED
@@ -1,6 +1,6 @@
1
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
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>
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/modpack-lock-tests.yml?logo=github&label=tests&logoColor=white&style=for-the-badge&labelColor=%23505050"></a>
4
4
 
5
5
  # modpack-lock
6
6
 
@@ -8,12 +8,11 @@
8
8
 
9
9
  Creates a modpack lockfile for files hosted on Modrinth (mods, resource packs, shaders and datapacks).
10
10
 
11
-
12
11
  ## Overview
13
12
 
14
13
  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
14
 
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.
15
+ This script generates a `modpack.lock` file in the current directory containing 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. An optional `modpack.json` file can also be created to store your modpack's metadata (name, version, modloader, dependencies, etc.) alongside the lockfile. This setup allows for easy diffing and clear version history.
17
16
 
18
17
  > 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
18
 
@@ -33,38 +32,88 @@ Alternatively, you can run it using `npx`:
33
32
  npx modpack-lock
34
33
  ```
35
34
 
36
- ## Usage
35
+ ## CLI
37
36
 
38
37
  Navigate to your Minecraft profile directory (the folder containing `mods`, `resourcepacks`, `datapacks`, and `shaderpacks` folders) and run:
39
38
 
39
+ ```bash
40
+ modpack-lock
41
+ ```
42
+
43
+ The script will:
44
+
45
+ 1. Scan the `mods`, `resourcepacks`, `datapacks`, and `shaderpacks` directories for `.jar` and `.zip` files
46
+ 2. Calculate SHA1 hashes for each file
47
+ 3. Query the Modrinth API to find version information
48
+ 4. Generate a `modpack.lock` file in the current directory
49
+
50
+ If a `modpack.json` file exists in the directory, the lockfile's dependency list will also be written to it. Run `modpack-lock init` to create this file.
51
+
52
+ Then, commit the `modpack.lock` (and `modpack.json`) to your repository and push it to your remote.
53
+
40
54
  ```text
41
- Usage: modpack-lock [options]
55
+ Usage: modpack-lock [options] [command]
42
56
 
43
- Create a modpack lockfile for files hosted on Modrinth (mods, resource packs, shaders and datapacks)
57
+ Creates a modpack lockfile for files hosted on Modrinth (mods, resource packs, shaders and datapacks)
44
58
 
45
59
  Options:
60
+ -p, --path <path> Path to the modpack directory
46
61
  -d, --dry-run Dry-run mode - no files will be written
62
+
63
+ GENERATION
47
64
  -g, --gitignore Print .gitignore rules for files not hosted on Modrinth
48
65
  -r, --readme Generate README.md files for each category
49
- -p, --path <path> Path to the modpack directory
50
66
 
51
67
  LOGGING
52
68
  -q, --quiet Quiet mode - only show errors and warnings
53
69
  -s, --silent Silent mode - no output
54
70
 
55
71
  INFORMATION
56
- -V, --version output the version number
57
- --help display help for modpack-lock
72
+ -V output the version number
73
+ -h, --help display help for modpack-lock
74
+
75
+ Commands:
76
+ init [options] This utility will walk you through creating a modpack.json file. It only covers the most common items, and tries to guess sensible defaults.
58
77
  ```
59
78
 
60
- The script will:
61
79
 
62
- 1. Scan the `mods`, `resourcepacks`, `datapacks`, and `shaderpacks` directories for `.jar` and `.zip` files
63
- 2. Calculate SHA1 hashes for each file
64
- 3. Query the Modrinth API to find version information
65
- 4. Generate a `modpack.lock` file in the current directory
80
+ ### Initialization
81
+
82
+ To initialize a new modpack, run:
83
+
84
+ ```bash
85
+ modpack-lock init
86
+ ```
87
+
88
+ This will create a `modpack.json` file that stores your modpack's metadata (name, version, author, etc.), including a list of dependency slugs. This file is optional, but when present, the main command will also write the lockfile dependencies to `modpack.json`. It will also regenerate the lockfile.
66
89
 
67
- Then, commit the `modpack.lock` file to your repository and push it to your remote.
90
+ The interactive mode will prompt you for each field. Set their initial values using the available option flags. Use `--noninteractive` with the required options (`--author`, `--modloader`, `--targetMinecraftVersion`) to skip prompts.
91
+
92
+ ```text
93
+ Usage: modpack-lock init [options]
94
+
95
+ This utility will walk you through creating a modpack.json file. It only covers the most common items, and tries to guess sensible defaults.
96
+
97
+ Options:
98
+ -f, --folder <path> Path to the modpack directory
99
+ -n, --noninteractive Non-interactive mode - must provide options for required fields
100
+
101
+ MODPACK INFORMATION
102
+ --name <name> Modpack name; defaults to the directory name; required
103
+ --version <version> Modpack version; defaults to 1.0.0; required
104
+ --id <id> Modpack slug/ID; defaults to the directory name slugified; required
105
+ --description <description> Modpack description
106
+ --author <author> Modpack author; required
107
+ --projectUrl <projectUrl> Modpack URL
108
+ --sourceUrl <sourceUrl> Modpack source code URL
109
+ --license <license> Modpack license
110
+ --modloader <modloader> Modpack modloader; required
111
+ --targetModloaderVersion <targetModloaderVersion> Target modloader version
112
+ --targetMinecraftVersion <targetMinecraftVersion> Target Minecraft version; required
113
+
114
+ INFORMATION
115
+ --help display help for modpack-lock init
116
+ ```
68
117
 
69
118
  > [!TIP]
70
119
  >
@@ -82,9 +131,26 @@ Then, commit the `modpack.lock` file to your repository and push it to your remo
82
131
  > # !mods/example.jar
83
132
  > ```
84
133
 
85
- ## Output
134
+ ## API
135
+
136
+ For programmatic usage, `modpack-lock` exports these functions:
137
+
138
+ - `getModpackInfo()`
139
+ - `getLockfile()`
140
+ - `generateJson()`
141
+ - `generateGitignoreRules()`
142
+ - `generateReadmeFiles()`
143
+ - `generateLockfile()`
144
+ - `generateModpackFiles()`
145
+ - `promptUserForInfo()`
146
+
147
+ See the [API documentation](https://nickesc.github.io/modpack-lock) for full details.
148
+
149
+ ## File Formats
150
+
151
+ ### `modpack.lock`
86
152
 
87
- The `modpack.lock` file has the following structure:
153
+ The lockfile contains metadata about Modrinth-hosted files found in your modpack directories:
88
154
 
89
155
  ```json
90
156
  {
@@ -111,6 +177,34 @@ The `modpack.lock` file has the following structure:
111
177
  }
112
178
  ```
113
179
 
180
+ ### `modpack.json`
181
+
182
+ If created via `modpack-lock init`, the JSON file combines your modpack metadata with the lockfile's dependency list:
183
+
184
+ ```json
185
+ {
186
+ "name": "My Modpack",
187
+ "version": "1.0.0",
188
+ "id": "my-modpack",
189
+ "description": "",
190
+ "author": "name",
191
+ "projectUrl": "",
192
+ "sourceUrl": "",
193
+ "license": "",
194
+ "modloader": "modloader",
195
+ "targetModloaderVersion": "",
196
+ "targetMinecraftVersion": "x.y.z",
197
+ "dependencies": {
198
+ "mods": [ ... ],
199
+ "resourcepacks": [ ... ],
200
+ "datapacks": [ ... ],
201
+ "shaderpacks": [ ... ]
202
+ }
203
+ }
204
+ ```
205
+
114
206
  ## License
115
207
 
116
208
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for more details.
209
+
210
+ <a href="https://github.com/nickesc/modpack-lock/blob/main/LICENSE"><img class="badge-img" alt="GitHub License" src="https://img.shields.io/github/license/nickesc/modpack-lock?style=for-the-badge&labelColor=%23333&color=%230070ff"></a>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "modpack-lock",
3
- "version": "0.2.0",
4
- "description": "Create a modpack lockfile for files hosted on Modrinth (mods, resource packs, shaders and datapacks)",
3
+ "version": "0.3.1",
4
+ "description": "Creates 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"
7
7
  },
@@ -9,12 +9,13 @@
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/nickesc/modpack-lock.git"
11
11
  },
12
+ "homepage": "https://nickesc.github.io/modpack-lock",
12
13
  "license": "MIT",
13
14
  "author": "N. Escobar <nick@nescobar.media> (https://nickesc.github.io/)",
14
15
  "type": "module",
15
- "main": "src/generate-lockfile.js",
16
+ "main": "src/modpack-lock.js",
16
17
  "bin": {
17
- "modpack-lock": "src/modpack-lock.js"
18
+ "modpack-lock": "src/cli.js"
18
19
  },
19
20
  "keywords": [
20
21
  "modrinth",
@@ -23,23 +24,29 @@
23
24
  "lockfile"
24
25
  ],
25
26
  "engines": {
26
- "node": ">=18.0.0"
27
+ "node": ">=22.0.0"
27
28
  },
28
29
  "scripts": {
29
30
  "test": "vitest --run",
30
- "start": "node src/modpack-lock.js",
31
- "modpack-lock": "node src/modpack-lock.js"
31
+ "start": "node src/cli.js",
32
+ "docs": "typedoc",
33
+ "modpack-lock": "node src/cli.js"
32
34
  },
33
35
  "files": [
34
36
  "README.md",
35
37
  "LICENSE",
36
38
  "package.json",
37
- "src/*.js"
39
+ "src/**/*.js"
38
40
  ],
39
41
  "dependencies": {
40
- "commander": "^14.0.2"
42
+ "commander": "^14.0.2",
43
+ "prompts": "^2.4.2",
44
+ "slugify": "^1.6.6"
41
45
  },
42
46
  "devDependencies": {
47
+ "@vitest/coverage-v8": "^4.0.16",
48
+ "typedoc": "^0.28.16",
49
+ "typedoc-github-theme": "^0.3.1",
43
50
  "unzipper": "^0.12.3",
44
51
  "vitest": "^4.0.16"
45
52
  }
package/src/cli.js ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env NODE_OPTIONS=--no-warnings node
2
+
3
+ import { Command } from 'commander';
4
+ import path from 'path';
5
+ import slugify from 'slugify';
6
+ import {generateLockfile} from './generate_lockfile.js';
7
+ import generateJson from './generate_json.js';
8
+ import { generateModpackFiles } from './modpack-lock.js';
9
+ import promptUserForInfo from './modpack_info.js';
10
+ import { getModpackInfo } from './directory_scanning.js';
11
+ import * as config from './config/index.js';
12
+ import pkg from '../package.json' with { type: 'json' };
13
+
14
+
15
+ const modpackLock = new Command('modpack-lock');
16
+
17
+ const originalLogs = {
18
+ log: console.log,
19
+ info: console.info,
20
+ warn: console.warn,
21
+ error: console.error,
22
+ };
23
+
24
+ /**
25
+ * Silence all console.log output
26
+ */
27
+ function quietConsole(silent = false) {
28
+ console.log = () => { };
29
+ console.info = () => { };
30
+ if (silent) {
31
+ console.warn = () => { };
32
+ console.error = () => { };
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Restore the console's original functions
38
+ */
39
+ function restoreConsole() {
40
+ console.log = originalLogs.log;
41
+ console.info = originalLogs.info;
42
+ console.warn = originalLogs.warn;
43
+ console.error = originalLogs.error;
44
+ }
45
+
46
+ modpackLock
47
+ .name(pkg.name)
48
+ .description(pkg.description)
49
+ .summary("Create a modpack lockfile")
50
+ .optionsGroup("Options:")
51
+ .option('-p, --path <path>', 'Path to the modpack directory')
52
+ .option('-d, --dry-run', 'Dry-run mode - no files will be written')
53
+ .optionsGroup("GENERATION")
54
+ .option('-g, --gitignore', 'Print .gitignore rules for files not hosted on Modrinth')
55
+ .option('-r, --readme', 'Generate README.md files for each category')
56
+ .optionsGroup("LOGGING")
57
+ .option('-q, --quiet', 'Quiet mode - only show errors and warnings')
58
+ .option('-s, --silent', 'Silent mode - no output')
59
+ .optionsGroup("INFORMATION")
60
+ .helpOption("-h, --help", `display help for ${pkg.name}`)
61
+ .version(pkg.version, '-V')
62
+ .action(async (options) => {
63
+ try {
64
+ const currDir = options.path || process.cwd();
65
+
66
+ if (options.quiet) {
67
+ quietConsole();
68
+ } else if (options.silent) {
69
+ quietConsole(true);
70
+ }
71
+
72
+ const modpackInfo = await getModpackInfo(currDir);
73
+ if (modpackInfo) {
74
+ await generateModpackFiles(modpackInfo, currDir, options);
75
+ } else {
76
+ await generateLockfile(currDir, options);
77
+ }
78
+ } catch (error) {
79
+ console.error('Error:', error);
80
+ process.exitCode = 1;
81
+ }
82
+ });
83
+
84
+ const jsonDescription = `This utility will walk you through creating a ${config.MODPACK_JSON_NAME} file. It only covers the most common items, and tries to guess sensible defaults.`;
85
+
86
+ modpackLock.command('init')
87
+ .description(jsonDescription)
88
+ .optionsGroup("Options:")
89
+ .option('-f, --folder <path>', 'Path to the modpack directory')
90
+ .option("-n, --noninteractive", 'Non-interactive mode - must provide options for required fields')
91
+ .optionsGroup("MODPACK INFORMATION")
92
+ .option('--name <name>', 'Modpack name; defaults to the directory name')
93
+ .option('--version <version>', 'Modpack version; defaults to 1.0.0')
94
+ .option('--id <id>', 'Modpack slug/ID; defaults to the directory name slugified')
95
+ .option('--description <description>', 'Modpack description')
96
+ .option('--author <author>', 'Modpack author; required')
97
+ .option('--projectUrl <projectUrl>', 'Modpack URL')
98
+ .option('--sourceUrl <sourceUrl>', 'Modpack source code URL')
99
+ .option('--license <license>', 'Modpack license')
100
+ .option('--modloader <modloader>', 'Modpack modloader; required')
101
+ .option('--targetModloaderVersion <targetModloaderVersion>', 'Target modloader version')
102
+ .option('--targetMinecraftVersion <targetMinecraftVersion>', 'Target Minecraft version; required')
103
+ .optionsGroup("INFORMATION")
104
+ .helpOption("--help", `display help for ${pkg.name} init`)
105
+ .action(async (options) => {
106
+ const currDir = options.folder || process.cwd();
107
+
108
+ if (options.noninteractive) {
109
+ quietConsole();
110
+ if (!options.author || !options.modloader || !options.targetMinecraftVersion) {
111
+ console.error('Error: Must provide options for required fields');
112
+ process.exitCode = 1;
113
+ return;
114
+ } else {
115
+ const name = options.name || path.basename(currDir);
116
+ const modpackInfo = {
117
+ name: name,
118
+ version: options.version || '1.0.0',
119
+ id: slugify(options.id || name, config.SLUGIFY_OPTIONS),
120
+ description: options.description || '',
121
+ author: options.author,
122
+ projectUrl: options.projectUrl || '',
123
+ sourceUrl: options.sourceUrl || '',
124
+ license: options.license || '',
125
+ modloader: options.modloader,
126
+ targetModloaderVersion: options.targetModloaderVersion || '',
127
+ targetMinecraftVersion: options.targetMinecraftVersion,
128
+ };
129
+ try {
130
+ await generateModpackFiles(modpackInfo, currDir, { dryRun: false });
131
+ } catch (error) {
132
+ console.error('Error:', error);
133
+ process.exitCode = 1;
134
+ }
135
+ }
136
+ } else {
137
+ console.log(jsonDescription);
138
+ console.log("\nSee `modpack-lock init --help` for definitive documentation on these fields and exactly what they do.\n");
139
+ console.log("Press ^C at any time to quit.\n");
140
+ try {
141
+ const modpackInfo = await promptUserForInfo({
142
+ name: options.name || path.basename(currDir),
143
+ version: options.version,
144
+ id: options.id,
145
+ description: options.description,
146
+ author: options.author,
147
+ projectUrl: options.projectUrl,
148
+ sourceUrl: options.sourceUrl,
149
+ license: options.license,
150
+ modloader: options.modloader,
151
+ targetModloaderVersion: options.targetModloaderVersion,
152
+ targetMinecraftVersion: options.targetMinecraftVersion,
153
+ });
154
+
155
+ await generateModpackFiles(modpackInfo, currDir, { dryRun: false });
156
+ } catch (error) {
157
+ console.error('Error:', error);
158
+ process.exitCode = 1;
159
+ }
160
+ }
161
+ });
162
+
163
+ modpackLock.parseAsync().catch((error) => {
164
+ console.error('Error:', error);
165
+ process.exit(1);
166
+ });
@@ -0,0 +1,14 @@
1
+ /** Modrinth API base URL */
2
+ export const MODRINTH_API_BASE = 'https://api.modrinth.com/v2';
3
+
4
+ /** Modrinth version files endpoint */
5
+ export const MODRINTH_VERSION_FILES_ENDPOINT = `${MODRINTH_API_BASE}/version_files`;
6
+
7
+ /** Modrinth projects endpoint */
8
+ export const MODRINTH_PROJECTS_ENDPOINT = `${MODRINTH_API_BASE}/projects`;
9
+
10
+ /** Modrinth users endpoint */
11
+ export const MODRINTH_USERS_ENDPOINT = `${MODRINTH_API_BASE}/users`;
12
+
13
+ /** Batch size for Modrinth API requests */
14
+ export const BATCH_SIZE = 100;
@@ -0,0 +1,20 @@
1
+ /** Lockfile format version -- increment on changes to the format */
2
+ export const LOCKFILE_VERSION = "1.0.1";
3
+
4
+ /** Required fields for the modpack information */
5
+ export const MODPACK_INFO_REQUIRED_FIELDS = [
6
+ "name",
7
+ "version",
8
+ "id",
9
+ "author",
10
+ "modloader",
11
+ "targetMinecraftVersion"
12
+ ];
13
+
14
+ /** Dependency categories, corresponds to folders in Minecraft profile */
15
+ export const DEPENDENCY_CATEGORIES = [
16
+ "mods",
17
+ "resourcepacks",
18
+ "shaderpacks",
19
+ "datapacks"
20
+ ];
@@ -0,0 +1,5 @@
1
+ /** Machine-readable/lockfile name */
2
+ export const MODPACK_LOCKFILE_NAME = "modpack.lock";
3
+
4
+ /** Human-readable/JSON file name */
5
+ export const MODPACK_JSON_NAME = "modpack.json";
@@ -0,0 +1,4 @@
1
+ export * from './constants.js';
2
+ export * from './api.js';
3
+ export * from './files.js';
4
+ export * from './options.js';
@@ -0,0 +1,2 @@
1
+ /** Options for slugify */
2
+ export const SLUGIFY_OPTIONS = { lower: true, strict: true, separator: '-', locale: 'en', trim: true };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @typedef {Object} ModpackInfo
3
+ * Contains information about the modpack that is not dependent on the lockfile.
4
+ * @property {string} name - The name of the modpack (Required)
5
+ * @property {string} version - The version of the modpack (Required)
6
+ * @property {string} description - The description of the modpack
7
+ * @property {string} id - The slug/ID of the modpack (Required)
8
+ * @property {string} author - The author of the modpack (Required)
9
+ * @property {string} projectUrl - The project URL of the modpack
10
+ * @property {string} sourceUrl - The source code URL of the modpack
11
+ * @property {string} license - The license of the modpack
12
+ * @property {string} modloader - The modloader of the modpack (Required)
13
+ * @property {string} targetModloaderVersion - The target modloader version of the modpack
14
+ * @property {string} targetMinecraftVersion - The target Minecraft version of the modpack (Required)
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} Lockfile
19
+ * Contains information about the modpack dependencies and their versions.
20
+ * @property {string} version - The version of the modpack
21
+ * @property {string} generated - The date and time the lockfile was generated
22
+ * @property {number} total - The total number of files in the modpack
23
+ * @property {Object} counts - The counts object
24
+ * @property {number} counts.mods - The mods count
25
+ * @property {number} counts.resourcepacks - The resourcepacks count
26
+ * @property {number} counts.datapacks - The datapacks count
27
+ * @property {number} counts.shaderpacks - The shaderpacks count
28
+ * @property {Object} dependencies - The dependencies object
29
+ * @property {Array<Object>} dependencies.mods - The mods object
30
+ * @property {Array<Object>} dependencies.resourcepacks - The resourcepacks object
31
+ * @property {Array<Object>} dependencies.datapacks - The datapacks object
32
+ * @property {Array<Object>} dependencies.shaderpacks - The shaderpacks object
33
+ */
34
+
35
+ /**
36
+ * @typedef {Object} Options
37
+ * Contains options for the generation of the modpack files.
38
+ * @property {boolean} dryRun - Whether to dry run the generation
39
+ * @property {boolean} quiet - Whether to quiet the console output
40
+ * @property {boolean} silent - Whether to silent the console output
41
+ * @property {boolean} gitignore - Whether to generate a .gitignore file
42
+ * @property {boolean} readme - Whether to generate README.md files
43
+ */
44
+
45
+ export {};
@@ -0,0 +1,118 @@
1
+ import fs from 'fs/promises';
2
+ import crypto from 'crypto';
3
+ import path from 'path';
4
+ import * as files from './config/files.js';
5
+ import * as constants from './config/constants.js';
6
+
7
+ /**
8
+ * @typedef {import('./config/types.js').ModpackInfo} ModpackInfo
9
+ * @typedef {import('./config/types.js').Lockfile} Lockfile
10
+ */
11
+
12
+ /**
13
+ * Get the directories to scan for modpack files
14
+ * @param {string} directoryPath - The path to the directory to scan
15
+ * @returns {Array<Object>} The directories to scan
16
+ */
17
+ export function getScanDirectories(directoryPath) {
18
+ const scanDirectories = [];
19
+ for (const category of constants.DEPENDENCY_CATEGORIES) {
20
+ scanDirectories.push({ name: category, path: path.join(directoryPath, category) });
21
+ }
22
+ return scanDirectories;
23
+ }
24
+
25
+ /**
26
+ * Calculate SHA1 hash of a file
27
+ */
28
+ async function calculateSHA1(filePath) {
29
+ const fileBuffer = await fs.readFile(filePath);
30
+ return crypto.createHash('sha1').update(fileBuffer).digest('hex');
31
+ }
32
+
33
+ /**
34
+ * Find all files in a directory
35
+ */
36
+ async function findFiles(dirPath) {
37
+ const files = [];
38
+
39
+ try {
40
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
41
+
42
+ for (const entry of entries) {
43
+ if (entry.isFile() && (entry.name.endsWith('.jar') || entry.name.endsWith('.zip'))) {
44
+ const fullPath = path.join(dirPath, entry.name);
45
+ files.push(fullPath);
46
+ }
47
+ }
48
+ } catch (error) {
49
+ if (error.code !== 'ENOENT') {
50
+ console.warn(`Warning: Could not read directory ${dirPath}: ${error.message}`);
51
+ }
52
+ }
53
+
54
+ files.sort((a, b) => a.localeCompare(b, 'en', { numeric: true, sensitivity: 'base' }));
55
+ return files;
56
+ }
57
+
58
+ /**
59
+ * Scan a directory and return file info with hashes
60
+ */
61
+ export async function scanDirectory(dirInfo, workspaceRoot) {
62
+ const files = await findFiles(dirInfo.path);
63
+ const fileEntries = [];
64
+
65
+ for (const filePath of files) {
66
+ try {
67
+ const hash = await calculateSHA1(filePath);
68
+ const relativePath = path.relative(workspaceRoot, filePath);
69
+
70
+ fileEntries.push({
71
+ path: relativePath,
72
+ fullPath: filePath,
73
+ hash: hash,
74
+ category: dirInfo.name,
75
+ });
76
+ } catch (error) {
77
+ console.warn(`Warning: Could not hash file ${filePath}: ${error.message}`);
78
+ }
79
+ }
80
+
81
+ return fileEntries;
82
+ }
83
+
84
+ /**
85
+ * Scan for existing JSON file and return the JSON object if it exists
86
+ */
87
+ async function getJsonFile(directoryPath, filename) {
88
+ const jsonPath = path.join(directoryPath, filename);
89
+ // try to read the file
90
+ try {
91
+ const fileContent = await fs.readFile(jsonPath, 'utf-8');
92
+ return JSON.parse(fileContent);
93
+ } catch (error) {
94
+ if (error.code !== 'ENOENT') {
95
+ throw new Error(`Error: Could not read file ${jsonPath}: ${error.message}`);
96
+ } else {
97
+ return null;
98
+ }
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Get the modpack info from the JSON file if it exists
104
+ * @param {string} directoryPath - The path to the directory to scan
105
+ * @returns {Promise<ModpackInfo|null>} The modpack info JSON object if the file exists, otherwise null
106
+ */
107
+ export async function getModpackInfo(directoryPath) {
108
+ return getJsonFile(directoryPath, files.MODPACK_JSON_NAME);
109
+ }
110
+
111
+ /**
112
+ * Get the lockfile file if it exists
113
+ * @param {string} directoryPath - The path to the directory to scan
114
+ * @returns {Lockfile|null} The JSON object if the file exists, otherwise null
115
+ */
116
+ export async function getLockfile(directoryPath) {
117
+ return getJsonFile(directoryPath, files.MODPACK_LOCKFILE_NAME);
118
+ }