@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.
- package/CHANGELOG.md +191 -0
- package/CONTRIBUTING.md +261 -0
- package/LICENSE.md +7 -0
- package/README.md +55 -0
- package/commands/auth.js +427 -0
- package/commands/config.js +356 -0
- package/commands/create.js +534 -0
- package/commands/info.js +113 -0
- package/commands/install.js +130 -0
- package/commands/launch.js +296 -0
- package/commands/search.js +50 -0
- package/commands/uninstall.js +94 -0
- package/commands/upgrade.js +343 -0
- package/commands/version.js +21 -0
- package/docs/README.md +119 -0
- package/docs/_config.yml +63 -0
- package/docs/commands/auth.md +376 -0
- package/docs/commands/config.md +294 -0
- package/docs/commands/create.md +276 -0
- package/docs/commands/info.md +460 -0
- package/docs/commands/install.md +320 -0
- package/docs/commands/launch.md +427 -0
- package/docs/commands/search.md +276 -0
- package/docs/commands/uninstall.md +128 -0
- package/docs/commands/upgrade.md +515 -0
- package/docs/commands.md +266 -0
- package/docs/configuration.md +410 -0
- package/docs/contributing.md +114 -0
- package/docs/index.md +82 -0
- package/docs/installation.md +162 -0
- package/helpers/auth/index.js +20 -0
- package/helpers/auth/microsoft.js +329 -0
- package/helpers/auth/storage.js +260 -0
- package/helpers/config.js +258 -0
- package/helpers/constants.js +38 -0
- package/helpers/getv.js +5 -0
- package/helpers/minecraft.js +308 -0
- package/helpers/modrinth.js +141 -0
- package/helpers/utils.js +334 -0
- package/helpers/versions.js +21 -0
- package/index.js +89 -0
- package/package.json +30 -0
|
@@ -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 };
|