@bobschlowinskii/clicraft 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.
@@ -0,0 +1,334 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import inquirer from 'inquirer';
5
+ import { pipeline } from 'stream/promises';
6
+ import { Readable } from 'stream';
7
+ import { USER_AGENT, PAGE_SIZE, NEXT_PAGE, PREV_PAGE } from './constants.js';
8
+
9
+ // ============================================
10
+ // HTTP Utilities
11
+ // ============================================
12
+
13
+ /**
14
+ * Fetch JSON from URL with proper user agent
15
+ */
16
+ export async function fetchJson(url) {
17
+ const response = await fetch(url, {
18
+ headers: { 'User-Agent': USER_AGENT }
19
+ });
20
+ if (!response.ok) {
21
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
22
+ }
23
+ return await response.json();
24
+ }
25
+
26
+ /**
27
+ * Download a file with optional progress indication
28
+ * @param {string} url - URL to download from
29
+ * @param {string} destPath - Destination file path
30
+ * @param {string} [description] - Optional description for progress display
31
+ * @param {boolean} [showProgress=true] - Whether to show progress
32
+ */
33
+ export async function downloadFile(url, destPath, description = null, showProgress = true) {
34
+ const response = await fetch(url, {
35
+ headers: { 'User-Agent': USER_AGENT }
36
+ });
37
+
38
+ if (!response.ok) {
39
+ throw new Error(`Download failed: ${response.status} ${response.statusText}`);
40
+ }
41
+
42
+ const totalSize = parseInt(response.headers.get('content-length') || '0');
43
+ const fileStream = fs.createWriteStream(destPath);
44
+
45
+ if (showProgress && description && totalSize > 0) {
46
+ let downloadedSize = 0;
47
+ const reader = response.body.getReader();
48
+
49
+ while (true) {
50
+ const { done, value } = await reader.read();
51
+ if (done) break;
52
+
53
+ downloadedSize += value.length;
54
+ const percent = Math.round((downloadedSize / totalSize) * 100);
55
+ process.stdout.write(`\r${chalk.gray(` ${description}: ${percent}%`)}`);
56
+
57
+ fileStream.write(Buffer.from(value));
58
+ }
59
+ process.stdout.write('\n');
60
+ fileStream.end();
61
+ } else {
62
+ await pipeline(Readable.fromWeb(response.body), fileStream);
63
+ if (showProgress && description) {
64
+ console.log(chalk.gray(` ${description}: Done`));
65
+ }
66
+ }
67
+ }
68
+
69
+ // ============================================
70
+ // Instance Config Utilities
71
+ // ============================================
72
+
73
+ /**
74
+ * Load mcconfig.json from an instance directory
75
+ * @param {string} instancePath - Path to instance directory
76
+ * @returns {object|null} - Config object or null if not found
77
+ */
78
+ export function loadConfig(instancePath) {
79
+ const configPath = path.join(instancePath, 'mcconfig.json');
80
+ if (!fs.existsSync(configPath)) {
81
+ return null;
82
+ }
83
+ try {
84
+ const content = fs.readFileSync(configPath, 'utf-8');
85
+ return JSON.parse(content);
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Save mcconfig.json to an instance directory
93
+ * @param {string} instancePath - Path to instance directory
94
+ * @param {object} config - Config object to save
95
+ */
96
+ export function saveConfig(instancePath, config) {
97
+ const configPath = path.join(instancePath, 'mcconfig.json');
98
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
99
+ }
100
+
101
+ /**
102
+ * Get instance path from options or current directory
103
+ * @param {object} options - Command options
104
+ * @returns {string} - Resolved instance path
105
+ */
106
+ export function getInstancePath(options) {
107
+ return options?.instance ? path.resolve(options.instance) : process.cwd();
108
+ }
109
+
110
+ /**
111
+ * Load config with error handling and user feedback
112
+ * @param {string} instancePath - Path to instance directory
113
+ * @returns {object|null} - Config object or null with error printed
114
+ */
115
+ export function requireConfig(instancePath) {
116
+ const config = loadConfig(instancePath);
117
+ if (!config) {
118
+ console.log(chalk.red('Error: No mcconfig.json found.'));
119
+ console.log(chalk.gray('Make sure you are in a Minecraft instance directory or use --instance <path>'));
120
+ return null;
121
+ }
122
+ return config;
123
+ }
124
+
125
+ // ============================================
126
+ // Maven/Library Utilities
127
+ // ============================================
128
+
129
+ /**
130
+ * Convert Maven coordinate to file path
131
+ * e.g., "org.ow2.asm:asm:9.9" -> "org/ow2/asm/asm/9.9/asm-9.9.jar"
132
+ * @param {string} name - Maven coordinate
133
+ * @returns {string|null} - File path or null if invalid
134
+ */
135
+ export function mavenToPath(name) {
136
+ const parts = name.split(':');
137
+ if (parts.length < 3) return null;
138
+
139
+ const [group, artifact, version] = parts;
140
+ const classifier = parts.length > 3 ? `-${parts[3]}` : '';
141
+ const groupPath = group.replace(/\./g, '/');
142
+
143
+ return `${groupPath}/${artifact}/${version}/${artifact}-${version}${classifier}.jar`;
144
+ }
145
+
146
+ // ============================================
147
+ // UI Utilities
148
+ // ============================================
149
+
150
+ /**
151
+ * Paginated selection prompt for large lists
152
+ * @param {string} message - Prompt message
153
+ * @param {Array} allChoices - All choices to paginate
154
+ * @param {Function} [getChoiceDisplay] - Function to get display text for a choice
155
+ * @returns {Promise<any>} - Selected value
156
+ */
157
+ export async function paginatedSelect(message, allChoices, getChoiceDisplay = (c) => c) {
158
+ let currentPage = 0;
159
+ const totalPages = Math.ceil(allChoices.length / PAGE_SIZE);
160
+
161
+ while (true) {
162
+ const startIdx = currentPage * PAGE_SIZE;
163
+ const endIdx = Math.min(startIdx + PAGE_SIZE, allChoices.length);
164
+ const pageChoices = allChoices.slice(startIdx, endIdx);
165
+
166
+ // Build choices for this page
167
+ const choices = pageChoices.map((choice) => ({
168
+ name: typeof choice === 'object' && choice.name ? choice.name : getChoiceDisplay(choice),
169
+ value: typeof choice === 'object' && choice.value !== undefined ? choice.value : choice
170
+ }));
171
+
172
+ // Add navigation options
173
+ if (currentPage > 0) {
174
+ choices.push({ name: chalk.cyan('← Previous page'), value: PREV_PAGE });
175
+ }
176
+ if (currentPage < totalPages - 1) {
177
+ choices.push({ name: chalk.cyan('→ Next page'), value: NEXT_PAGE });
178
+ }
179
+
180
+ const pageInfo = totalPages > 1 ? ` (page ${currentPage + 1}/${totalPages})` : '';
181
+
182
+ const { selection } = await inquirer.prompt([
183
+ {
184
+ type: 'rawlist',
185
+ name: 'selection',
186
+ message: `${message}${pageInfo}`,
187
+ choices: choices
188
+ }
189
+ ]);
190
+
191
+ if (selection === NEXT_PAGE) {
192
+ currentPage++;
193
+ continue;
194
+ } else if (selection === PREV_PAGE) {
195
+ currentPage--;
196
+ continue;
197
+ }
198
+
199
+ return selection;
200
+ }
201
+ }
202
+
203
+ // ============================================
204
+ // Formatting Utilities
205
+ // ============================================
206
+
207
+ /**
208
+ * Format bytes to human readable string
209
+ * @param {number} bytes - Byte count
210
+ * @returns {string} - Formatted string (e.g., "1.5 MB")
211
+ */
212
+ export function formatBytes(bytes) {
213
+ if (bytes === 0) return '0 B';
214
+ const k = 1024;
215
+ const sizes = ['B', 'KB', 'MB', 'GB'];
216
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
217
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
218
+ }
219
+
220
+ /**
221
+ * Format download count for display
222
+ * @param {number} count - Download count
223
+ * @returns {string} - Formatted string (e.g., "1.5M")
224
+ */
225
+ export function formatDownloads(count) {
226
+ if (count >= 1000000) {
227
+ return (count / 1000000).toFixed(1) + 'M';
228
+ } else if (count >= 1000) {
229
+ return (count / 1000).toFixed(1) + 'K';
230
+ }
231
+ return count.toString();
232
+ }
233
+
234
+ /**
235
+ * Format ISO date string to readable format
236
+ * @param {string} isoString - ISO date string
237
+ * @returns {string} - Formatted date string
238
+ */
239
+ export function formatDate(isoString) {
240
+ if (!isoString) return 'Unknown';
241
+ const date = new Date(isoString);
242
+ return date.toLocaleDateString('en-US', {
243
+ year: 'numeric',
244
+ month: 'long',
245
+ day: 'numeric',
246
+ hour: '2-digit',
247
+ minute: '2-digit'
248
+ });
249
+ }
250
+
251
+ // ============================================
252
+ // File System Utilities
253
+ // ============================================
254
+
255
+ /**
256
+ * Get directory size recursively
257
+ * @param {string} dirPath - Directory path
258
+ * @returns {number} - Size in bytes
259
+ */
260
+ export function getDirSize(dirPath) {
261
+ if (!fs.existsSync(dirPath)) return 0;
262
+
263
+ let size = 0;
264
+ const files = fs.readdirSync(dirPath, { withFileTypes: true });
265
+
266
+ for (const file of files) {
267
+ const filePath = path.join(dirPath, file.name);
268
+ if (file.isDirectory()) {
269
+ size += getDirSize(filePath);
270
+ } else {
271
+ try {
272
+ size += fs.statSync(filePath).size;
273
+ } catch {
274
+ // Skip files we can't read
275
+ }
276
+ }
277
+ }
278
+
279
+ return size;
280
+ }
281
+
282
+ /**
283
+ * Count files in directory recursively
284
+ * @param {string} dirPath - Directory path
285
+ * @param {string} [extension] - Optional extension filter
286
+ * @returns {number} - File count
287
+ */
288
+ export function countFiles(dirPath, extension = null) {
289
+ if (!fs.existsSync(dirPath)) return 0;
290
+
291
+ let count = 0;
292
+ const files = fs.readdirSync(dirPath, { withFileTypes: true });
293
+
294
+ for (const file of files) {
295
+ const filePath = path.join(dirPath, file.name);
296
+ if (file.isDirectory()) {
297
+ count += countFiles(filePath, extension);
298
+ } else if (!extension || file.name.endsWith(extension)) {
299
+ count++;
300
+ }
301
+ }
302
+
303
+ return count;
304
+ }
305
+
306
+ /**
307
+ * Parse a value string into appropriate type (boolean, number, or string)
308
+ * @param {string} value - Value to parse
309
+ * @returns {boolean|number|string|null} - Parsed value
310
+ */
311
+ export function parseValue(value) {
312
+ if (value === 'null' || value === 'auto') return null;
313
+ if (value === 'true') return true;
314
+ if (value === 'false') return false;
315
+ if (!isNaN(value) && value !== '') return parseFloat(value);
316
+ return value;
317
+ }
318
+
319
+ export default {
320
+ fetchJson,
321
+ downloadFile,
322
+ loadConfig,
323
+ saveConfig,
324
+ getInstancePath,
325
+ requireConfig,
326
+ mavenToPath,
327
+ paginatedSelect,
328
+ formatBytes,
329
+ formatDownloads,
330
+ formatDate,
331
+ getDirSize,
332
+ countFiles,
333
+ parseValue
334
+ };
@@ -0,0 +1,21 @@
1
+ const versions = [
2
+ '0.1.0',
3
+ '0.2.0',
4
+ '0.2.1',
5
+ '0.2.2',
6
+ '0.3.0',
7
+ '0.3.1',
8
+ '0.3.2',
9
+ '0.4.0',
10
+ ];
11
+
12
+
13
+ function enumerate(version) {
14
+ return versions.indexOf(version) ?? -2;
15
+ }
16
+
17
+ function denumerate(index) {
18
+ return versions[index] ?? null;
19
+ }
20
+
21
+ export { enumerate, denumerate };
package/index.js ADDED
@@ -0,0 +1,89 @@
1
+ #! /usr/bin/env node
2
+
3
+ import { createInstance } from './commands/create.js';
4
+ import { searchMods } from './commands/search.js';
5
+ import { installMod } from './commands/install.js';
6
+ import { uninstallMod } from './commands/uninstall.js';
7
+ import { authCommand } from './commands/auth.js';
8
+ import { launchInstance } from './commands/launch.js';
9
+ import { instanceInfo } from './commands/info.js';
10
+ import { upgrade } from './commands/upgrade.js';
11
+ import { showVersion } from './commands/version.js';
12
+ import { configCommand } from './commands/config.js';
13
+
14
+ import {program} from 'commander';
15
+
16
+ program
17
+ .option('-v, --version', 'Show the curent version')
18
+ .action(showVersion)
19
+
20
+ program
21
+ .command('search <query>')
22
+ .description('Search for Minecraft mods on Modrinth')
23
+ .option('-l, --limit <number>', 'Number of results to show', '10')
24
+ .option('-v, --version <version>', 'Filter by Minecraft version')
25
+ .option('--loader <loader>', 'Filter by mod loader (fabric, forge, quilt, neoforge)')
26
+ .option('--verbose', 'Enable verbose output')
27
+ .action(searchMods);
28
+
29
+ program
30
+ .command('create')
31
+ .description('Create a new Minecraft instance')
32
+ .option('--verbose', 'Enable verbose output')
33
+ .action(createInstance);
34
+
35
+ program
36
+ .command('install <mod>')
37
+ .description('Install a mod to the current Minecraft instance')
38
+ .option('-i, --instance <path>', 'Path to the instance directory')
39
+ .option('-f, --force', 'Force reinstall/update if already installed')
40
+ .option('--verbose', 'Enable verbose output')
41
+ .action(installMod);
42
+
43
+ program
44
+ .command('uninstall [mod]')
45
+ .description('Uninstall a mod from the current Minecraft instance')
46
+ .option('-i, --instance <path>', 'Path to the instance directory')
47
+ .option('-f, --force', 'Skip confirmation prompt')
48
+ .option('--verbose', 'Enable verbose output')
49
+ .action(uninstallMod);
50
+
51
+ program
52
+ .command('auth [action] [args...]')
53
+ .description('Manage Minecraft accounts (login, logout, switch, status)')
54
+ .option('-f, --force', 'Skip confirmation prompts')
55
+ .option('--verbose', 'Enable verbose output')
56
+ .action(authCommand);
57
+
58
+ program
59
+ .command('launch')
60
+ .description('Launch the Minecraft instance')
61
+ .option('-i, --instance <path>', 'Path to the instance directory')
62
+ .option('--offline', 'Launch in offline mode')
63
+ .option('--verbose', 'Enable verbose output')
64
+ .action(launchInstance);
65
+
66
+ program
67
+ .command('info')
68
+ .description('Show information about the current Minecraft instance')
69
+ .option('-i, --instance <path>', 'Path to the instance directory')
70
+ .option('--verbose', 'Show detailed information')
71
+ .option('--mods', 'List installed mods')
72
+ .action(instanceInfo);
73
+
74
+ program
75
+ .command('upgrade [mod]')
76
+ .description('Upgrade mods, Minecraft version, or mod loader')
77
+ .option('-i, --instance <path>', 'Path to the instance directory')
78
+ .option('-f, --force', 'Force upgrade even if already up to date')
79
+ .option('--verbose', 'Enable verbose output')
80
+ .action(upgrade);
81
+
82
+ program
83
+ .command('config [action] [args...]')
84
+ .description('Manage CLI settings and game settings')
85
+ .option('-i, --instance <path>', 'Path to the instance directory')
86
+ .option('--verbose', 'Show detailed output')
87
+ .action(configCommand);
88
+
89
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@bobschlowinskii/clicraft",
3
+ "version": "0.4.0",
4
+ "description": "A simple Minecraft Mod Package Manager written in Node.JS",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "bin": {
11
+ "clicraft": "./index.js"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/theinfamousben/clicraft.git"
16
+ },
17
+ "author": "theinfamousben",
18
+ "license": "ISC",
19
+ "bugs": {
20
+ "url": "https://github.com/theinfamousben/clicraft/issues"
21
+ },
22
+ "homepage": "https://github.com/theinfamousben/clicraft#readme",
23
+ "dependencies": {
24
+ "chalk": "^5.6.2",
25
+ "commander": "^14.0.2",
26
+ "conf": "^15.0.2",
27
+ "inquirer": "^13.2.0",
28
+ "open": "^11.0.0"
29
+ }
30
+ }