modpack-lock 0.3.1 → 0.4.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 CHANGED
@@ -18,6 +18,8 @@ This script generates a `modpack.lock` file in the current directory containing
18
18
 
19
19
  The lockfile could also serve as a basis for restoring modpack contents after cloning the repository to a new machine.
20
20
 
21
+ Using the `scripts` field in `modpack.json`, you can also define reusable, tracked shell commands for common modpack tasks (like publishing, generating assets or CI/CD workflows).
22
+
21
23
  ## Installation
22
24
 
23
25
  To install the script globally with `npm`:
@@ -115,6 +117,38 @@ INFORMATION
115
117
  --help display help for modpack-lock init
116
118
  ```
117
119
 
120
+ ### Running Scripts
121
+
122
+ To run a script defined in `modpack.json` run:
123
+
124
+ ```bash
125
+ modpack-lock run <script>
126
+ ```
127
+
128
+ This command takes the name of the script as its first argument. Use the `-f` option to specify a different path to the modpack directory. For debug logging, use the `-D` option.
129
+
130
+ To pass additional arguments and options to the script, write them after a `--` separator:
131
+
132
+ ```bash
133
+ modpack-lock run <script> -- [options] <args>
134
+ ```
135
+
136
+ The `scripts` field in `modpack.json` is a key-value pair of script names and their corresponding shell commands. The `scripts` field is optional and is omitted by default.
137
+
138
+ ```text
139
+ Usage: modpack-lock run [options] <script>
140
+
141
+ Run a script (shell command) defined in modpack.json's 'scripts' object
142
+
143
+ Arguments:
144
+ script The name of the script to run
145
+
146
+ Options:
147
+ -f, --folder <path> Path to the modpack directory
148
+ -D, --debug Debug mode -- show more information about how the command is being parsed
149
+ -h, --help display help for modpack-lock run
150
+ ```
151
+
118
152
  > [!TIP]
119
153
  >
120
154
  > 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.
@@ -199,6 +233,9 @@ If created via `modpack-lock init`, the JSON file combines your modpack metadata
199
233
  "resourcepacks": [ ... ],
200
234
  "datapacks": [ ... ],
201
235
  "shaderpacks": [ ... ]
236
+ },
237
+ "scripts": {
238
+ "example": "echo 'example script'"
202
239
  }
203
240
  }
204
241
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modpack-lock",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
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"
package/src/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env NODE_OPTIONS=--no-warnings node
2
2
 
3
3
  import { Command } from 'commander';
4
- import path from 'path';
5
4
  import slugify from 'slugify';
5
+ import path from 'path';
6
+ import { spawn } from 'child_process';
6
7
  import {generateLockfile} from './generate_lockfile.js';
7
- import generateJson from './generate_json.js';
8
8
  import { generateModpackFiles } from './modpack-lock.js';
9
9
  import promptUserForInfo from './modpack_info.js';
10
10
  import { getModpackInfo } from './directory_scanning.js';
@@ -43,6 +43,28 @@ function restoreConsole() {
43
43
  console.error = originalLogs.error;
44
44
  }
45
45
 
46
+ /**
47
+ * Merge modpack info with priority: options > existingInfo > defaults
48
+ * Preserves all fields from existingInfo
49
+ */
50
+ function mergeModpackInfo(existingInfo, options, defaults) {
51
+ const result = {};
52
+ for (const [key, defaultValue] of Object.entries(defaults)) {
53
+ result[key] = options[key] || existingInfo?.[key] || defaultValue;
54
+ }
55
+
56
+ // Then, add any fields from existingInfo that aren't in defaults
57
+ if (existingInfo) {
58
+ for (const [key, value] of Object.entries(existingInfo)) {
59
+ if (!(key in defaults)) {
60
+ result[key] = value;
61
+ }
62
+ }
63
+ }
64
+
65
+ return result;
66
+ }
67
+
46
68
  modpackLock
47
69
  .name(pkg.name)
48
70
  .description(pkg.description)
@@ -101,31 +123,36 @@ modpackLock.command('init')
101
123
  .option('--targetModloaderVersion <targetModloaderVersion>', 'Target modloader version')
102
124
  .option('--targetMinecraftVersion <targetMinecraftVersion>', 'Target Minecraft version; required')
103
125
  .optionsGroup("INFORMATION")
104
- .helpOption("--help", `display help for ${pkg.name} init`)
126
+ .helpOption("-h, --help", `display help for ${pkg.name} init`)
105
127
  .action(async (options) => {
106
128
  const currDir = options.folder || process.cwd();
107
129
 
130
+ let existingInfo = await getModpackInfo(currDir);
131
+
108
132
  if (options.noninteractive) {
109
133
  quietConsole();
110
- if (!options.author || !options.modloader || !options.targetMinecraftVersion) {
134
+ if ( (!options.author && !existingInfo?.author) || (!options.modloader && !existingInfo?.modloader) || (!options.targetMinecraftVersion && !existingInfo?.targetMinecraftVersion)) {
111
135
  console.error('Error: Must provide options for required fields');
112
136
  process.exitCode = 1;
113
137
  return;
114
138
  } 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,
139
+ const defaultName = path.basename(currDir);
140
+ const defaults = {
141
+ name: defaultName,
142
+ version: config.DEFAULT_MODPACK_VERSION,
143
+ id: defaultName,
144
+ description: '',
145
+ author: options.author, // Required, no default
146
+ projectUrl: '',
147
+ sourceUrl: '',
148
+ license: '',
149
+ modloader: options.modloader, // Required, no default
150
+ targetModloaderVersion: '',
151
+ targetMinecraftVersion: options.targetMinecraftVersion, // Required, no default
128
152
  };
153
+
154
+ const modpackInfo = mergeModpackInfo(existingInfo, options, defaults);
155
+ modpackInfo.id = slugify(modpackInfo.id, config.SLUGIFY_OPTIONS);
129
156
  try {
130
157
  await generateModpackFiles(modpackInfo, currDir, { dryRun: false });
131
158
  } catch (error) {
@@ -138,19 +165,23 @@ modpackLock.command('init')
138
165
  console.log("\nSee `modpack-lock init --help` for definitive documentation on these fields and exactly what they do.\n");
139
166
  console.log("Press ^C at any time to quit.\n");
140
167
  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
- });
168
+ const defaults = {
169
+ name: path.basename(currDir),
170
+ version: config.DEFAULT_MODPACK_VERSION,
171
+ id: undefined,
172
+ description: undefined,
173
+ author: undefined,
174
+ projectUrl: undefined,
175
+ sourceUrl: undefined,
176
+ license: config.DEFAULT_MODPACK_LICENSE,
177
+ modloader: undefined,
178
+ targetModloaderVersion: undefined,
179
+ targetMinecraftVersion: undefined,
180
+ };
181
+
182
+ const modpackInfo = await promptUserForInfo(
183
+ mergeModpackInfo(existingInfo, options, defaults)
184
+ );
154
185
 
155
186
  await generateModpackFiles(modpackInfo, currDir, { dryRun: false });
156
187
  } catch (error) {
@@ -160,6 +191,69 @@ modpackLock.command('init')
160
191
  }
161
192
  });
162
193
 
194
+ modpackLock.command('run')
195
+ .description(`Run a script (shell command) defined in ${config.MODPACK_JSON_NAME}\'s \'scripts\' object`)
196
+ .argument('<script>', 'The name of the script to run')
197
+ .option('-f, --folder <path>', 'Path to the modpack directory')
198
+ .option('-D, --debug', 'Debug mode -- show more information about how the command is being parsed')
199
+ .helpOption("-h, --help", `display help for ${pkg.name} run`)
200
+ .allowExcessArguments(true)
201
+ .allowUnknownOption(true)
202
+ .action(async (script, options, command) => {
203
+ try {
204
+ if (options.debug) {
205
+ console.log("COMMAND:", command);
206
+ }
207
+
208
+ const currDir = options.folder || process.cwd();
209
+ const modpackInfo = await getModpackInfo(currDir);
210
+
211
+ // verify neccecary files and information exist
212
+ if (!modpackInfo) {
213
+ throw new Error('No modpack.json file found');
214
+ }
215
+ if (!modpackInfo.scripts) {
216
+ throw new Error('No scripts defined in modpack.json');
217
+ }
218
+ if (!modpackInfo.scripts[script]) {
219
+ throw new Error(`Script ${script} not found in modpack.json`);
220
+ }
221
+
222
+ // build the full command
223
+ const scriptCommand = modpackInfo.scripts[script];
224
+ const args = command.args ? command.args.slice(1) : [];
225
+ const fullCommand = `${scriptCommand} ${args.join(' ')}`;
226
+
227
+ // debug logging
228
+ if (options.debug) {
229
+ console.log("CURR DIR:", currDir);
230
+ console.log("OPTIONS:", options);
231
+ console.log("SCRIPT:", script);
232
+ console.log("SCRIPT COMMAND:", scriptCommand);
233
+ console.log("ARGS:", args);
234
+ console.log("FULL COMMAND:", fullCommand);
235
+ }
236
+
237
+ // spawn the command
238
+ const child = spawn(fullCommand, [], {
239
+ shell: true,
240
+ stdio: 'inherit',
241
+ cwd: currDir
242
+ });
243
+
244
+ // preserve exit code on completion
245
+ const exitCode = await new Promise((resolve) => {
246
+ child.on('close', (code) => {
247
+ resolve(code || 0);
248
+ });
249
+ });
250
+ process.exitCode = exitCode;
251
+ } catch (error) {
252
+ console.error('Error:', error.message);
253
+ process.exitCode = 1;
254
+ }
255
+ });
256
+
163
257
  modpackLock.parseAsync().catch((error) => {
164
258
  console.error('Error:', error);
165
259
  process.exit(1);
@@ -0,0 +1,8 @@
1
+ export const DEFAULT_MODPACK_VERSION = '1.0.0';
2
+ export const DEFAULT_MODPACK_LICENSE = 'MIT';
3
+ export const DEFAULT_PROJECT_URL = (id) => {
4
+ return `https://modrinth.com/modpack/${id}`;
5
+ };
6
+ export const DEFAULT_SOURCE_URL = (id, author) => {
7
+ return `https://github.com/${author}/${id}`;
8
+ };
@@ -2,3 +2,4 @@ export * from './constants.js';
2
2
  export * from './api.js';
3
3
  export * from './files.js';
4
4
  export * from './options.js';
5
+ export * from './defaults.js';
@@ -33,64 +33,63 @@ export default async function promptUserForInfo(defaults = {}) {
33
33
  return validateNotEmpty(value, 'Name');
34
34
  },
35
35
  });
36
- let answers = await prompts([
37
- {
38
- type: 'text',
39
- name: 'version',
40
- message: 'Modpack version',
41
- initial: defaults.version || '1.0.0',
42
- validate: (value) => {
43
- return validateNotEmpty(value, 'Version');
44
- },
45
- },
46
-
47
- {
48
- type: 'text',
49
- name: 'id',
50
- message: 'Modpack slug/ID',
51
- initial: slugify(defaults.id || name.name, config.SLUGIFY_OPTIONS),
52
- validate: (value) => {
53
- return validateNotEmpty(value, 'ID');
54
- },
36
+ let version = await prompts({
37
+ type: 'text',
38
+ name: 'version',
39
+ message: 'Modpack version',
40
+ initial: defaults.version || config.DEFAULT_MODPACK_VERSION,
41
+ validate: (value) => {
42
+ return validateNotEmpty(value, 'Version');
55
43
  },
56
- {
57
- type: 'text',
58
- name: 'description',
59
- message: 'Modpack description',
60
- initial: defaults.description || undefined,
44
+ });
45
+ let id = await prompts({
46
+ type: 'text',
47
+ name: 'id',
48
+ message: 'Modpack slug/ID',
49
+ initial: slugify(defaults.id || name.name, config.SLUGIFY_OPTIONS),
50
+ validate: (value) => {
51
+ return validateNotEmpty(value, 'ID');
61
52
  },
62
- {
63
- type: 'text',
64
- name: 'author',
65
- message: 'Modpack author',
66
- initial: defaults.author || undefined,
67
- validate: (value) => {
68
- return validateNotEmpty(value, 'Author');
69
- },
53
+ });
54
+ let description = await prompts({
55
+ type: 'text',
56
+ name: 'description',
57
+ message: 'Modpack description',
58
+ initial: defaults.description,
59
+ });
60
+ let author = await prompts({
61
+ type: 'text',
62
+ name: 'author',
63
+ message: 'Modpack author',
64
+ initial: defaults.author,
65
+ validate: (value) => {
66
+ return validateNotEmpty(value, 'Author');
70
67
  },
68
+ });
69
+ let answers = await prompts([
71
70
  {
72
71
  type: 'text',
73
72
  name: 'projectUrl',
74
73
  message: 'Modpack URL',
75
- initial: defaults.projectUrl || undefined,
74
+ initial: defaults.projectUrl || config.DEFAULT_PROJECT_URL(id.id),
76
75
  },
77
76
  {
78
77
  type: 'text',
79
78
  name: 'sourceUrl',
80
79
  message: 'Modpack source code URL',
81
- initial: defaults.sourceUrl || undefined,
80
+ initial: defaults.sourceUrl || config.DEFAULT_SOURCE_URL(id.id, author.author),
82
81
  },
83
82
  {
84
83
  type: 'text',
85
84
  name: 'license',
86
85
  message: 'Modpack license',
87
- initial: defaults.license || undefined,
86
+ initial: defaults.license || config.DEFAULT_MODPACK_LICENSE,
88
87
  },
89
88
  {
90
89
  type: 'autocomplete',
91
90
  name: 'modloader',
92
91
  message: 'Modpack modloader',
93
- initial: defaults.modloader || undefined,
92
+ initial: defaults.modloader,
94
93
  choices: [
95
94
  { title: 'fabric' },
96
95
  { title: 'forge' },
@@ -114,20 +113,20 @@ export default async function promptUserForInfo(defaults = {}) {
114
113
  type: 'text',
115
114
  name: 'targetModloaderVersion',
116
115
  message: 'Target modloader version',
117
- initial: defaults.targetModloaderVersion || undefined,
116
+ initial: defaults.targetModloaderVersion,
118
117
  },
119
118
  {
120
119
  type: 'text',
121
120
  name: 'targetMinecraftVersion',
122
121
  message: 'Target Minecraft version',
123
- initial: defaults.targetMinecraftVersion || undefined,
122
+ initial: defaults.targetMinecraftVersion,
124
123
  validate: (value) => {
125
124
  return validateNotEmpty(value, 'Minecraft Version');
126
125
  },
127
126
  }
128
127
  ]);
129
128
 
130
- let modpackInfo = {...name, ...answers};
129
+ let modpackInfo = {...name, ...version, ...id, ...description, ...author, ...answers};
131
130
  if (Object.keys(modpackInfo).length < 11) {
132
131
  console.warn('Modpack initialization was interrupted');
133
132
  process.exit(1);