@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,130 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { downloadFile, loadConfig, saveConfig, getInstancePath, requireConfig } from '../helpers/utils.js';
5
+ import { getProject, getProjectVersions } from '../helpers/modrinth.js';
6
+
7
+ export async function installMod(modSlug, options) {
8
+ const instancePath = getInstancePath(options);
9
+
10
+ const config = requireConfig(instancePath);
11
+ if (!config) return;
12
+
13
+ console.log(chalk.cyan(`\nšŸ“¦ Installing "${modSlug}" to ${config.name}...\n`));
14
+
15
+ try {
16
+ // Get project info
17
+ const project = await getProject(modSlug);
18
+ if (!project) {
19
+ console.log(chalk.red(`Error: Mod "${modSlug}" not found on Modrinth.`));
20
+ console.log(chalk.gray('Use "clicraft search <query>" to find available mods.'));
21
+ return;
22
+ }
23
+
24
+ if (project.project_type !== 'mod') {
25
+ console.log(chalk.red(`Error: "${modSlug}" is a ${project.project_type}, not a mod.`));
26
+ return;
27
+ }
28
+
29
+ console.log(chalk.gray(`Found: ${project.title}`));
30
+ console.log(chalk.gray(`Looking for ${config.modLoader} version for Minecraft ${config.minecraftVersion}...`));
31
+
32
+ // Get compatible versions
33
+ const versions = await getProjectVersions(modSlug, config.minecraftVersion, config.modLoader);
34
+
35
+ if (versions.length === 0) {
36
+ console.log(chalk.red(`\nNo compatible version found for ${config.modLoader} on Minecraft ${config.minecraftVersion}`));
37
+
38
+ // Show available versions
39
+ const allVersions = await getProjectVersions(modSlug);
40
+ if (allVersions.length > 0) {
41
+ const loaders = [...new Set(allVersions.flatMap(v => v.loaders))];
42
+ const gameVersions = [...new Set(allVersions.flatMap(v => v.game_versions))].slice(0, 10);
43
+ console.log(chalk.gray(`\nAvailable loaders: ${loaders.join(', ')}`));
44
+ console.log(chalk.gray(`Recent game versions: ${gameVersions.join(', ')}`));
45
+ }
46
+ return;
47
+ }
48
+
49
+ // Use the latest compatible version
50
+ const version = versions[0];
51
+ const file = version.files.find(f => f.primary) || version.files[0];
52
+
53
+ if (!file) {
54
+ console.log(chalk.red('Error: No downloadable file found for this version.'));
55
+ return;
56
+ }
57
+
58
+ // Check if already installed
59
+ const existingMod = config.mods.find(m => m.projectId === project.id);
60
+ if (existingMod && !options.force) {
61
+ console.log(chalk.yellow(`\nāš ļø ${project.title} is already installed (version ${existingMod.versionNumber})`));
62
+ console.log(chalk.gray('Use --force to reinstall or update.'));
63
+ return;
64
+ }
65
+
66
+ // Create mods folder if needed
67
+ const modsPath = path.join(instancePath, 'mods');
68
+ if (!fs.existsSync(modsPath)) {
69
+ fs.mkdirSync(modsPath, { recursive: true });
70
+ }
71
+
72
+ // Remove old version if updating
73
+ if (existingMod) {
74
+ const oldFilePath = path.join(modsPath, existingMod.fileName);
75
+ if (fs.existsSync(oldFilePath)) {
76
+ fs.unlinkSync(oldFilePath);
77
+ console.log(chalk.gray(`Removed old version: ${existingMod.fileName}`));
78
+ }
79
+ config.mods = config.mods.filter(m => m.projectId !== project.id);
80
+ }
81
+
82
+ // Download the mod
83
+ const destPath = path.join(modsPath, file.filename);
84
+ console.log(chalk.gray(`Downloading ${file.filename}...`));
85
+ await downloadFile(file.url, destPath, null, false);
86
+
87
+ // Update config
88
+ config.mods.push({
89
+ projectId: project.id,
90
+ slug: project.slug,
91
+ name: project.title,
92
+ versionId: version.id,
93
+ versionNumber: version.version_number,
94
+ fileName: file.filename,
95
+ installedAt: new Date().toISOString()
96
+ });
97
+
98
+ saveConfig(instancePath, config);
99
+
100
+ console.log(chalk.green(`\nāœ… Successfully installed ${project.title} v${version.version_number}`));
101
+ console.log(chalk.gray(` File: mods/${file.filename}`));
102
+
103
+ // Show dependencies if any
104
+ if (version.dependencies?.length > 0) {
105
+ const requiredDeps = version.dependencies.filter(d => d.dependency_type === 'required');
106
+ if (requiredDeps.length > 0) {
107
+ console.log(chalk.yellow(`\nāš ļø This mod has ${requiredDeps.length} required dependencies:`));
108
+ for (const dep of requiredDeps) {
109
+ if (dep.project_id) {
110
+ const depProject = await getProject(dep.project_id);
111
+ if (depProject) {
112
+ const isInstalled = config.mods.some(m => m.projectId === dep.project_id);
113
+ const status = isInstalled ? chalk.green('āœ“') : chalk.red('āœ—');
114
+ console.log(chalk.gray(` ${status} ${depProject.title} (${depProject.slug})`));
115
+ }
116
+ }
117
+ }
118
+ console.log(chalk.gray('\nInstall dependencies with: clicraft install <slug>'));
119
+ }
120
+ }
121
+
122
+ } catch (error) {
123
+ console.error(chalk.red('Error installing mod:'), error.message);
124
+ if (options.verbose) {
125
+ console.error(error);
126
+ }
127
+ }
128
+ }
129
+
130
+ export default { installMod };
@@ -0,0 +1,296 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { spawn } from 'child_process';
5
+ import { refreshAuth } from './auth.js';
6
+ import { captureGameSettings } from '../commands/config.js';
7
+ import { loadSettings, writeGameSettings } from '../helpers/config.js';
8
+ import {
9
+ loadConfig,
10
+ getInstancePath,
11
+ requireConfig,
12
+ mavenToPath
13
+ } from '../helpers/utils.js';
14
+
15
+ // Find Java executable
16
+ function findJava() {
17
+ const javaHome = process.env.JAVA_HOME;
18
+ if (javaHome) {
19
+ const javaBin = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
20
+ if (fs.existsSync(javaBin)) {
21
+ return javaBin;
22
+ }
23
+ }
24
+ return 'java';
25
+ }
26
+
27
+ // Get classpath separator
28
+ function getClasspathSeparator() {
29
+ return process.platform === 'win32' ? ';' : ':';
30
+ }
31
+
32
+ // Build classpath from libraries
33
+ function buildClasspath(instancePath, versionData) {
34
+ const sep = getClasspathSeparator();
35
+ const librariesPath = path.join(instancePath, 'libraries');
36
+ const classpath = [];
37
+
38
+ for (const lib of versionData.libraries || []) {
39
+ // Check OS rules
40
+ if (lib.rules) {
41
+ let dominated = false;
42
+ for (const rule of lib.rules) {
43
+ if (rule.os?.name) {
44
+ const osName = process.platform === 'darwin' ? 'osx' :
45
+ process.platform === 'win32' ? 'windows' : 'linux';
46
+ if (rule.action === 'allow' && rule.os.name === osName) {
47
+ dominated = true;
48
+ break;
49
+ }
50
+ } else if (rule.action === 'allow') {
51
+ dominated = true;
52
+ break;
53
+ }
54
+ }
55
+ if (!dominated) continue;
56
+ }
57
+
58
+ let libPath = null;
59
+
60
+ if (lib.downloads?.artifact) {
61
+ libPath = path.join(librariesPath, lib.downloads.artifact.path);
62
+ } else if (lib.name) {
63
+ const relativePath = mavenToPath(lib.name);
64
+ if (relativePath) {
65
+ libPath = path.join(librariesPath, relativePath);
66
+ }
67
+ }
68
+
69
+ if (libPath && fs.existsSync(libPath)) {
70
+ classpath.push(libPath);
71
+ }
72
+ }
73
+
74
+ return classpath.join(sep);
75
+ }
76
+
77
+ // Replace argument variables
78
+ function replaceArgVariables(arg, variables) {
79
+ let result = arg;
80
+ for (const [key, value] of Object.entries(variables)) {
81
+ result = result.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value);
82
+ }
83
+ return result;
84
+ }
85
+
86
+ // Parse arguments from version JSON
87
+ function parseArguments(args, variables) {
88
+ const result = [];
89
+
90
+ for (const arg of args) {
91
+ if (typeof arg === 'string') {
92
+ result.push(replaceArgVariables(arg, variables));
93
+ } else if (typeof arg === 'object') {
94
+ let allowed = arg.rules?.some(rule => {
95
+ if (rule.os?.name) {
96
+ const osName = process.platform === 'darwin' ? 'osx' :
97
+ process.platform === 'win32' ? 'windows' : 'linux';
98
+ return rule.action === 'allow' && rule.os.name === osName;
99
+ }
100
+ if (rule.features) return false;
101
+ return rule.action === 'allow';
102
+ }) ?? true;
103
+
104
+ if (allowed && arg.value) {
105
+ const values = Array.isArray(arg.value) ? arg.value : [arg.value];
106
+ for (const v of values) {
107
+ result.push(replaceArgVariables(v, variables));
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ return result;
114
+ }
115
+
116
+ export async function launchInstance(options) {
117
+ const settings = loadSettings();
118
+ const instancePath = getInstancePath(options);
119
+
120
+ // Apply saved game settings if enabled
121
+ if (settings.autoLoadConfigOnLaunch) {
122
+ const config = loadConfig(instancePath);
123
+ if (config?.gameSettings) {
124
+ writeGameSettings(instancePath, config.gameSettings);
125
+ if (options.verbose) {
126
+ console.log(chalk.gray('Applied saved game settings to options.txt'));
127
+ }
128
+ }
129
+ }
130
+
131
+ const config = requireConfig(instancePath);
132
+ if (!config) return;
133
+
134
+ if (config.type === 'server') {
135
+ console.log(chalk.red('Error: This is a server instance. Use ./start.sh to start the server.'));
136
+ return;
137
+ }
138
+
139
+ console.log(chalk.cyan(`\nšŸŽ® Launching ${config.name}...\n`));
140
+
141
+ try {
142
+ // Get authentication
143
+ let auth = null;
144
+ if (!options.offline) {
145
+ auth = await refreshAuth();
146
+ if (!auth) {
147
+ console.log(chalk.yellow('Not logged in. Use "clicraft login" to authenticate.'));
148
+ console.log(chalk.gray('Or use --offline to play offline (if available).'));
149
+ return;
150
+ }
151
+ console.log(chalk.gray(`Logged in as: ${auth.username}`));
152
+ } else {
153
+ console.log(chalk.yellow('Launching in offline mode...'));
154
+ auth = {
155
+ uuid: '00000000000000000000000000000000',
156
+ username: 'Player',
157
+ accessToken: 'offline'
158
+ };
159
+ }
160
+
161
+ // Find version JSON
162
+ const versionId = config.versionId;
163
+ let versionData;
164
+
165
+ const fabricVersionPath = path.join(instancePath, 'versions', versionId, `${versionId}.json`);
166
+ const vanillaVersionPath = path.join(instancePath, 'versions', config.minecraftVersion, `${config.minecraftVersion}.json`);
167
+
168
+ if (fs.existsSync(fabricVersionPath)) {
169
+ versionData = JSON.parse(fs.readFileSync(fabricVersionPath, 'utf-8'));
170
+
171
+ if (versionData.inheritsFrom && fs.existsSync(vanillaVersionPath)) {
172
+ const parentData = JSON.parse(fs.readFileSync(vanillaVersionPath, 'utf-8'));
173
+ versionData.libraries = [...(versionData.libraries || []), ...(parentData.libraries || [])];
174
+ versionData.assetIndex = versionData.assetIndex || parentData.assetIndex;
175
+ versionData.assets = versionData.assets || parentData.assets;
176
+ if (parentData.arguments) {
177
+ versionData.arguments = versionData.arguments || {};
178
+ versionData.arguments.jvm = [...(parentData.arguments?.jvm || []), ...(versionData.arguments?.jvm || [])];
179
+ versionData.arguments.game = [...(versionData.arguments?.game || []), ...(parentData.arguments?.game || [])];
180
+ }
181
+ }
182
+ } else if (fs.existsSync(vanillaVersionPath)) {
183
+ versionData = JSON.parse(fs.readFileSync(vanillaVersionPath, 'utf-8'));
184
+ } else {
185
+ console.log(chalk.red('Error: Version data not found.'));
186
+ console.log(chalk.gray(`Expected: ${fabricVersionPath}`));
187
+ return;
188
+ }
189
+
190
+ // Find client JAR
191
+ const clientJarPath = path.join(instancePath, 'versions', config.minecraftVersion, `${config.minecraftVersion}.jar`);
192
+ if (!fs.existsSync(clientJarPath)) {
193
+ console.log(chalk.red('Error: Minecraft client JAR not found.'));
194
+ return;
195
+ }
196
+
197
+ // Build classpath
198
+ const sep = getClasspathSeparator();
199
+ const classpath = buildClasspath(instancePath, versionData) + sep + clientJarPath;
200
+
201
+ // Set up natives directory
202
+ const nativesPath = path.join(instancePath, 'natives');
203
+ fs.mkdirSync(nativesPath, { recursive: true });
204
+
205
+ const variables = {
206
+ 'auth_player_name': auth.username,
207
+ 'auth_uuid': auth.uuid,
208
+ 'auth_access_token': auth.accessToken,
209
+ 'auth_xuid': '',
210
+ 'user_type': 'msa',
211
+ 'version_name': versionId,
212
+ 'version_type': 'release',
213
+ 'game_directory': instancePath,
214
+ 'assets_root': path.join(instancePath, 'assets'),
215
+ 'assets_index_name': versionData.assets || versionData.assetIndex?.id || config.minecraftVersion,
216
+ 'natives_directory': nativesPath,
217
+ 'launcher_name': 'clicraft',
218
+ 'launcher_version': '0.1.0',
219
+ 'classpath': classpath,
220
+ 'clientid': '',
221
+ 'user_properties': '{}',
222
+ 'resolution_width': '854',
223
+ 'resolution_height': '480'
224
+ };
225
+
226
+ // Build JVM arguments
227
+ const jvmArgs = [
228
+ `-Djava.library.path=${nativesPath}`,
229
+ '-Xmx2G',
230
+ '-Xms512M',
231
+ '-XX:+UnlockExperimentalVMOptions',
232
+ '-XX:+UseG1GC',
233
+ '-XX:G1NewSizePercent=20',
234
+ '-XX:G1ReservePercent=20',
235
+ '-XX:MaxGCPauseMillis=50',
236
+ '-XX:G1HeapRegionSize=32M',
237
+ '-cp', classpath
238
+ ];
239
+
240
+ const mainClass = versionData.mainClass;
241
+
242
+ // Build game arguments
243
+ let gameArgs = [];
244
+ if (versionData.arguments?.game) {
245
+ gameArgs = parseArguments(versionData.arguments.game, variables);
246
+ } else if (versionData.minecraftArguments) {
247
+ gameArgs = versionData.minecraftArguments.split(' ').map(arg => replaceArgVariables(arg, variables));
248
+ }
249
+
250
+ const java = findJava();
251
+ const fullArgs = [...jvmArgs, mainClass, ...gameArgs];
252
+
253
+ console.log(chalk.gray(`Java: ${java}`));
254
+ console.log(chalk.gray(`Main class: ${mainClass}`));
255
+ console.log(chalk.gray(`Game directory: ${instancePath}`));
256
+ console.log();
257
+
258
+ if (options.verbose) {
259
+ console.log(chalk.gray('Full command:'));
260
+ console.log(chalk.gray(`${java} ${fullArgs.join(' ')}`));
261
+ console.log();
262
+ }
263
+
264
+ console.log(chalk.green('šŸš€ Starting Minecraft...\n'));
265
+
266
+ const minecraft = spawn(java, fullArgs, {
267
+ cwd: instancePath,
268
+ stdio: 'inherit',
269
+ detached: false
270
+ });
271
+
272
+ minecraft.on('error', (err) => {
273
+ console.error(chalk.red('Failed to start Minecraft:'), err.message);
274
+ });
275
+
276
+ minecraft.on('close', (code) => {
277
+ if (settings.autoSaveToConfig) {
278
+ captureGameSettings({ instance: instancePath, verbose: options.verbose }, settings.autoSaveToConfig);
279
+ }
280
+
281
+ if (code === 0) {
282
+ console.log(chalk.green('\nMinecraft closed normally.'));
283
+ } else {
284
+ console.log(chalk.yellow(`\nMinecraft exited with code ${code}`));
285
+ }
286
+ });
287
+
288
+ } catch (error) {
289
+ console.error(chalk.red('Error launching Minecraft:'), error.message);
290
+ if (options.verbose) {
291
+ console.error(error);
292
+ }
293
+ }
294
+ }
295
+
296
+ export default { launchInstance };
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+ import { searchMods as searchModrinth } from '../helpers/modrinth.js';
3
+ import { formatDownloads } from '../helpers/utils.js';
4
+
5
+ export async function searchMods(query, options) {
6
+ if (!query) {
7
+ console.log(chalk.red('Error: Please provide a search query'));
8
+ console.log(chalk.gray('Usage: clicraft search <query> [options]'));
9
+ return;
10
+ }
11
+
12
+ console.log(chalk.cyan(`\nšŸ” Searching for "${query}" on Modrinth...\n`));
13
+
14
+ try {
15
+ const results = await searchModrinth(query, {
16
+ limit: options.limit || 10,
17
+ version: options.version,
18
+ loader: options.loader
19
+ });
20
+
21
+ if (results.hits.length === 0) {
22
+ console.log(chalk.yellow('No mods found matching your search.'));
23
+ return;
24
+ }
25
+
26
+ console.log(chalk.gray(`Found ${results.total_hits} results (showing ${results.hits.length}):\n`));
27
+
28
+ results.hits.forEach((mod, index) => {
29
+ const downloads = formatDownloads(mod.downloads);
30
+ const loaders = mod.categories?.filter(c =>
31
+ ['fabric', 'forge', 'quilt', 'neoforge'].includes(c)
32
+ ).join(', ') || 'Unknown';
33
+
34
+ console.log(chalk.bold.white(`${index + 1}. ${mod.title}`));
35
+ console.log(chalk.gray(` Slug: ${chalk.cyan(mod.slug)}`));
36
+ console.log(chalk.gray(` ${mod.description}`));
37
+ console.log(chalk.gray(` šŸ“„ ${chalk.green(downloads)} downloads | šŸ”§ ${chalk.yellow(loaders)}`));
38
+ console.log(chalk.gray(` šŸ”— https://modrinth.com/mod/${mod.slug}`));
39
+ console.log();
40
+ });
41
+
42
+ } catch (error) {
43
+ console.error(chalk.red('Error searching Modrinth:'), error.message);
44
+ if (options.verbose) {
45
+ console.error(error);
46
+ }
47
+ }
48
+ }
49
+
50
+ export default { searchMods };
@@ -0,0 +1,94 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import inquirer from 'inquirer';
5
+ import { loadConfig, saveConfig, getInstancePath, requireConfig } from '../helpers/utils.js';
6
+ import { findMod } from '../helpers/modrinth.js';
7
+
8
+ export async function uninstallMod(modSlug, options) {
9
+ const instancePath = getInstancePath(options);
10
+
11
+ const config = requireConfig(instancePath);
12
+ if (!config) return;
13
+
14
+ // If no mod specified, show interactive selection
15
+ if (!modSlug) {
16
+ if (!config.mods || config.mods.length === 0) {
17
+ console.log(chalk.yellow('\nNo mods installed in this instance.'));
18
+ return;
19
+ }
20
+
21
+ const { selectedMods } = await inquirer.prompt([{
22
+ type: 'checkbox',
23
+ name: 'selectedMods',
24
+ message: 'Select mods to uninstall:',
25
+ choices: config.mods.map(m => ({
26
+ name: `${m.name} (${m.versionNumber})`,
27
+ value: m.slug
28
+ }))
29
+ }]);
30
+
31
+ if (selectedMods.length === 0) {
32
+ console.log(chalk.yellow('\nNo mods selected.'));
33
+ return;
34
+ }
35
+
36
+ for (const slug of selectedMods) {
37
+ await uninstallSingleMod(instancePath, config, slug, options);
38
+ }
39
+ return;
40
+ }
41
+
42
+ await uninstallSingleMod(instancePath, config, modSlug, options);
43
+ }
44
+
45
+ async function uninstallSingleMod(instancePath, config, modSlug, options) {
46
+ const mod = findMod(config.mods, modSlug);
47
+
48
+ if (!mod) {
49
+ console.log(chalk.red(`Error: Mod "${modSlug}" is not installed.`));
50
+ console.log(chalk.gray('Use "clicraft info --mods" to see installed mods.'));
51
+ return;
52
+ }
53
+
54
+ console.log(chalk.cyan(`\nšŸ—‘ļø Uninstalling "${mod.name}"...\n`));
55
+
56
+ if (!options.force) {
57
+ const { confirm } = await inquirer.prompt([{
58
+ type: 'confirm',
59
+ name: 'confirm',
60
+ message: `Are you sure you want to uninstall ${mod.name}?`,
61
+ default: false
62
+ }]);
63
+
64
+ if (!confirm) {
65
+ console.log(chalk.yellow('Uninstall cancelled.'));
66
+ return;
67
+ }
68
+ }
69
+
70
+ try {
71
+ const modsPath = path.join(instancePath, 'mods');
72
+ const modFilePath = path.join(modsPath, mod.fileName);
73
+
74
+ if (fs.existsSync(modFilePath)) {
75
+ fs.unlinkSync(modFilePath);
76
+ console.log(chalk.gray(`Deleted: mods/${mod.fileName}`));
77
+ } else {
78
+ console.log(chalk.yellow(`Warning: Mod file not found: mods/${mod.fileName}`));
79
+ }
80
+
81
+ config.mods = config.mods.filter(m => m.projectId !== mod.projectId);
82
+ saveConfig(instancePath, config);
83
+
84
+ console.log(chalk.green(`\nāœ… Successfully uninstalled ${mod.name}`));
85
+
86
+ } catch (error) {
87
+ console.error(chalk.red('Error uninstalling mod:'), error.message);
88
+ if (options.verbose) {
89
+ console.error(error);
90
+ }
91
+ }
92
+ }
93
+
94
+ export default { uninstallMod };