@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,308 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { fetchJson, downloadFile, mavenToPath } from './utils.js';
5
+ import {
6
+ MOJANG_VERSION_MANIFEST,
7
+ MOJANG_RESOURCES,
8
+ FABRIC_META,
9
+ FABRIC_MAVEN,
10
+ FORGE_PROMOS,
11
+ FORGE_MAVEN,
12
+ USER_AGENT
13
+ } from './constants.js';
14
+
15
+ // ============================================
16
+ // Mojang/Minecraft APIs
17
+ // ============================================
18
+
19
+ /**
20
+ * Fetch available Minecraft versions (releases only)
21
+ * @returns {Promise<Array>} - Array of version objects
22
+ */
23
+ export async function fetchMinecraftVersions() {
24
+ const data = await fetchJson(MOJANG_VERSION_MANIFEST);
25
+ return data.versions.filter(v => v.type === 'release');
26
+ }
27
+
28
+ /**
29
+ * Get version manifest for a specific Minecraft version
30
+ * @param {string} mcVersion - Minecraft version
31
+ * @returns {Promise<object>} - Version manifest data
32
+ */
33
+ export async function getVersionManifest(mcVersion) {
34
+ const manifest = await fetchJson(MOJANG_VERSION_MANIFEST);
35
+ const versionEntry = manifest.versions.find(v => v.id === mcVersion);
36
+ if (!versionEntry) {
37
+ throw new Error(`Minecraft version ${mcVersion} not found`);
38
+ }
39
+ return await fetchJson(versionEntry.url);
40
+ }
41
+
42
+ // ============================================
43
+ // Fabric APIs
44
+ // ============================================
45
+
46
+ /**
47
+ * Fetch Fabric loader versions
48
+ * @returns {Promise<Array<string>>} - Array of version strings
49
+ */
50
+ export async function fetchFabricLoaderVersions() {
51
+ const data = await fetchJson(`${FABRIC_META}/versions/loader`);
52
+ return data.map(v => v.version);
53
+ }
54
+
55
+ /**
56
+ * Fetch Fabric-supported game versions
57
+ * @returns {Promise<Array<string>>} - Array of stable Minecraft versions
58
+ */
59
+ export async function fetchFabricGameVersions() {
60
+ const data = await fetchJson(`${FABRIC_META}/versions/game`);
61
+ return data.filter(v => v.stable).map(v => v.version);
62
+ }
63
+
64
+ /**
65
+ * Fetch Fabric profile JSON for a version
66
+ * @param {string} mcVersion - Minecraft version
67
+ * @param {string} loaderVersion - Fabric loader version
68
+ * @returns {Promise<object>} - Fabric profile data
69
+ */
70
+ export async function fetchFabricProfile(mcVersion, loaderVersion) {
71
+ return await fetchJson(`${FABRIC_META}/versions/loader/${mcVersion}/${loaderVersion}/profile/json`);
72
+ }
73
+
74
+ /**
75
+ * Fetch Fabric installer versions
76
+ * @returns {Promise<Array>} - Array of installer version objects
77
+ */
78
+ export async function fetchFabricInstallerVersions() {
79
+ return await fetchJson(`${FABRIC_META}/versions/installer`);
80
+ }
81
+
82
+ // ============================================
83
+ // Forge APIs
84
+ // ============================================
85
+
86
+ /**
87
+ * Fetch Forge versions for a specific Minecraft version
88
+ * @param {string} mcVersion - Minecraft version
89
+ * @returns {Promise<Array>} - Array of Forge version objects
90
+ */
91
+ export async function fetchForgeVersions(mcVersion) {
92
+ const data = await fetchJson(FORGE_PROMOS);
93
+ const versions = [];
94
+ for (const [key, value] of Object.entries(data.promos)) {
95
+ if (key.startsWith(mcVersion)) {
96
+ versions.push({ label: key, version: value });
97
+ }
98
+ }
99
+ return versions;
100
+ }
101
+
102
+ /**
103
+ * Fetch all Forge-supported Minecraft versions
104
+ * @returns {Promise<Array<string>>} - Array of Minecraft versions
105
+ */
106
+ export async function fetchForgeGameVersions() {
107
+ const data = await fetchJson(FORGE_PROMOS);
108
+ const mcVersions = new Set();
109
+ for (const key of Object.keys(data.promos)) {
110
+ const mcVersion = key.split('-')[0];
111
+ mcVersions.add(mcVersion);
112
+ }
113
+ return Array.from(mcVersions).sort((a, b) => {
114
+ const aParts = a.split('.').map(Number);
115
+ const bParts = b.split('.').map(Number);
116
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
117
+ const aVal = aParts[i] || 0;
118
+ const bVal = bParts[i] || 0;
119
+ if (aVal !== bVal) return bVal - aVal;
120
+ }
121
+ return 0;
122
+ });
123
+ }
124
+
125
+ // ============================================
126
+ // Library Download
127
+ // ============================================
128
+
129
+ /**
130
+ * Check if library applies to current OS
131
+ * @param {object} lib - Library object with rules
132
+ * @returns {boolean} - Whether library should be used
133
+ */
134
+ function checkLibraryRules(lib) {
135
+ if (!lib.rules) return true;
136
+
137
+ const osName = process.platform === 'darwin' ? 'osx' :
138
+ process.platform === 'win32' ? 'windows' : 'linux';
139
+
140
+ return lib.rules.some(rule => {
141
+ if (rule.os && rule.os.name) {
142
+ return rule.action === 'allow' && rule.os.name === osName;
143
+ }
144
+ return rule.action === 'allow';
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Download libraries from version data
150
+ * @param {object} versionData - Version manifest data
151
+ * @param {string} instancePath - Instance directory path
152
+ */
153
+ export async function downloadLibraries(versionData, instancePath) {
154
+ const librariesPath = path.join(instancePath, 'libraries');
155
+ fs.mkdirSync(librariesPath, { recursive: true });
156
+
157
+ const libraries = versionData.libraries || [];
158
+ const applicableLibs = libraries.filter(checkLibraryRules);
159
+ let downloadCount = 0;
160
+
161
+ for (const lib of applicableLibs) {
162
+ let libPath = null;
163
+ let downloadUrl = null;
164
+
165
+ // Format 1: downloads.artifact (vanilla Minecraft)
166
+ if (lib.downloads?.artifact) {
167
+ const artifact = lib.downloads.artifact;
168
+ libPath = path.join(librariesPath, artifact.path);
169
+ downloadUrl = artifact.url;
170
+ }
171
+ // Format 2: name with Maven coordinates + url (Fabric/Forge)
172
+ else if (lib.name && lib.url) {
173
+ const relativePath = mavenToPath(lib.name);
174
+ if (relativePath) {
175
+ libPath = path.join(librariesPath, relativePath);
176
+ downloadUrl = lib.url + relativePath;
177
+ }
178
+ }
179
+ // Format 3: name only (defaults to Fabric Maven)
180
+ else if (lib.name) {
181
+ const relativePath = mavenToPath(lib.name);
182
+ if (relativePath) {
183
+ libPath = path.join(librariesPath, relativePath);
184
+ downloadUrl = `${FABRIC_MAVEN}/${relativePath}`;
185
+ }
186
+ }
187
+
188
+ if (libPath && downloadUrl) {
189
+ // Skip if already downloaded
190
+ if (fs.existsSync(libPath)) {
191
+ downloadCount++;
192
+ continue;
193
+ }
194
+
195
+ fs.mkdirSync(path.dirname(libPath), { recursive: true });
196
+
197
+ try {
198
+ await downloadFile(downloadUrl, libPath, `Library ${++downloadCount}/${applicableLibs.length}`);
199
+ } catch (err) {
200
+ console.log(chalk.yellow(` Warning: Failed to download ${lib.name}`));
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Download assets from version data
208
+ * @param {object} versionData - Version manifest data
209
+ * @param {string} instancePath - Instance directory path
210
+ */
211
+ export async function downloadAssets(versionData, instancePath) {
212
+ const assetsPath = path.join(instancePath, 'assets');
213
+ const indexesPath = path.join(assetsPath, 'indexes');
214
+ const objectsPath = path.join(assetsPath, 'objects');
215
+
216
+ fs.mkdirSync(indexesPath, { recursive: true });
217
+ fs.mkdirSync(objectsPath, { recursive: true });
218
+
219
+ // Download asset index
220
+ const assetIndex = versionData.assetIndex;
221
+ const indexPath = path.join(indexesPath, `${assetIndex.id}.json`);
222
+
223
+ if (!fs.existsSync(indexPath)) {
224
+ await downloadFile(assetIndex.url, indexPath, 'Asset index');
225
+ }
226
+
227
+ const assets = await fetchJson(assetIndex.url);
228
+ const objects = Object.entries(assets.objects);
229
+
230
+ console.log(chalk.gray(` Downloading ${objects.length} assets...`));
231
+
232
+ let downloaded = 0;
233
+ for (const [name, obj] of objects) {
234
+ const hash = obj.hash;
235
+ const prefix = hash.substring(0, 2);
236
+ const objectDir = path.join(objectsPath, prefix);
237
+ const objectPath = path.join(objectDir, hash);
238
+
239
+ if (fs.existsSync(objectPath)) {
240
+ downloaded++;
241
+ continue;
242
+ }
243
+
244
+ fs.mkdirSync(objectDir, { recursive: true });
245
+
246
+ const url = `${MOJANG_RESOURCES}/${prefix}/${hash}`;
247
+ try {
248
+ const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
249
+ if (response.ok) {
250
+ const buffer = await response.arrayBuffer();
251
+ fs.writeFileSync(objectPath, Buffer.from(buffer));
252
+ }
253
+ } catch (err) {
254
+ // Skip failed assets
255
+ }
256
+
257
+ downloaded++;
258
+ process.stdout.write(`\r${chalk.gray(` Assets: ${downloaded}/${objects.length}`)}`);
259
+ }
260
+ process.stdout.write(`\r${chalk.gray(` Assets: ${downloaded}/${objects.length}`)}\n`);
261
+ }
262
+
263
+ /**
264
+ * Build classpath from version data
265
+ * @param {string} instancePath - Instance directory path
266
+ * @param {object} versionData - Version manifest data
267
+ * @returns {string} - Classpath string
268
+ */
269
+ export function buildClasspath(instancePath, versionData) {
270
+ const sep = process.platform === 'win32' ? ';' : ':';
271
+ const librariesPath = path.join(instancePath, 'libraries');
272
+ const classpath = [];
273
+
274
+ for (const lib of versionData.libraries || []) {
275
+ if (!checkLibraryRules(lib)) continue;
276
+
277
+ let libPath = null;
278
+
279
+ if (lib.downloads?.artifact) {
280
+ libPath = path.join(librariesPath, lib.downloads.artifact.path);
281
+ } else if (lib.name) {
282
+ const relativePath = mavenToPath(lib.name);
283
+ if (relativePath) {
284
+ libPath = path.join(librariesPath, relativePath);
285
+ }
286
+ }
287
+
288
+ if (libPath && fs.existsSync(libPath)) {
289
+ classpath.push(libPath);
290
+ }
291
+ }
292
+
293
+ return classpath.join(sep);
294
+ }
295
+
296
+ export default {
297
+ fetchMinecraftVersions,
298
+ getVersionManifest,
299
+ fetchFabricLoaderVersions,
300
+ fetchFabricGameVersions,
301
+ fetchFabricProfile,
302
+ fetchFabricInstallerVersions,
303
+ fetchForgeVersions,
304
+ fetchForgeGameVersions,
305
+ downloadLibraries,
306
+ downloadAssets,
307
+ buildClasspath
308
+ };
@@ -0,0 +1,141 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fetchJson, downloadFile } from './utils.js';
4
+ import { MODRINTH_API, USER_AGENT } from './constants.js';
5
+
6
+ /**
7
+ * Get project info from Modrinth
8
+ * @param {string} slugOrId - Project slug or ID
9
+ * @returns {Promise<object|null>} - Project data or null if not found
10
+ */
11
+ export async function getProject(slugOrId) {
12
+ try {
13
+ return await fetchJson(`${MODRINTH_API}/project/${slugOrId}`);
14
+ } catch (error) {
15
+ if (error.message.includes('404')) {
16
+ return null;
17
+ }
18
+ throw error;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Get project versions from Modrinth
24
+ * @param {string} slugOrId - Project slug or ID
25
+ * @param {string} [mcVersion] - Filter by Minecraft version
26
+ * @param {string} [loader] - Filter by mod loader
27
+ * @returns {Promise<Array>} - Array of version objects
28
+ */
29
+ export async function getProjectVersions(slugOrId, mcVersion = null, loader = null) {
30
+ const params = new URLSearchParams();
31
+ if (mcVersion) {
32
+ params.set('game_versions', JSON.stringify([mcVersion]));
33
+ }
34
+ if (loader) {
35
+ params.set('loaders', JSON.stringify([loader]));
36
+ }
37
+
38
+ const query = params.toString();
39
+ const url = `${MODRINTH_API}/project/${slugOrId}/version${query ? '?' + query : ''}`;
40
+ return await fetchJson(url);
41
+ }
42
+
43
+ /**
44
+ * Search for mods on Modrinth
45
+ * @param {string} query - Search query
46
+ * @param {object} options - Search options
47
+ * @param {number} [options.limit=10] - Max results
48
+ * @param {string} [options.version] - Minecraft version filter
49
+ * @param {string} [options.loader] - Mod loader filter
50
+ * @returns {Promise<object>} - Search results
51
+ */
52
+ export async function searchMods(query, options = {}) {
53
+ const params = new URLSearchParams({
54
+ query: query,
55
+ limit: options.limit || 10,
56
+ facets: JSON.stringify([['project_type:mod']])
57
+ });
58
+
59
+ // Add version filter if specified
60
+ if (options.version) {
61
+ const facets = JSON.parse(params.get('facets'));
62
+ facets.push([`versions:${options.version}`]);
63
+ params.set('facets', JSON.stringify(facets));
64
+ }
65
+
66
+ // Add loader filter if specified
67
+ if (options.loader) {
68
+ const facets = JSON.parse(params.get('facets'));
69
+ facets.push([`categories:${options.loader}`]);
70
+ params.set('facets', JSON.stringify(facets));
71
+ }
72
+
73
+ return await fetchJson(`${MODRINTH_API}/search?${params}`);
74
+ }
75
+
76
+ /**
77
+ * Download a mod from Modrinth
78
+ * @param {string} projectId - Modrinth project ID
79
+ * @param {string} mcVersion - Minecraft version
80
+ * @param {string} loader - Mod loader (fabric, forge, etc.)
81
+ * @param {string} modsPath - Path to mods directory
82
+ * @returns {Promise<object|null>} - Mod info object or null if failed
83
+ */
84
+ export async function downloadMod(projectId, mcVersion, loader, modsPath) {
85
+ // Get project info
86
+ const project = await getProject(projectId);
87
+ if (!project) {
88
+ return null;
89
+ }
90
+
91
+ // Get compatible versions
92
+ const versions = await getProjectVersions(projectId, mcVersion, loader);
93
+ if (versions.length === 0) {
94
+ return null;
95
+ }
96
+
97
+ // Use the latest compatible version
98
+ const modVersion = versions[0];
99
+ const file = modVersion.files.find(f => f.primary) || modVersion.files[0];
100
+
101
+ if (!file) {
102
+ return null;
103
+ }
104
+
105
+ // Download the file
106
+ const destPath = path.join(modsPath, file.filename);
107
+ await downloadFile(file.url, destPath, project.title);
108
+
109
+ return {
110
+ projectId: project.id,
111
+ slug: project.slug,
112
+ name: project.title,
113
+ versionId: modVersion.id,
114
+ versionNumber: modVersion.version_number,
115
+ fileName: file.filename,
116
+ installedAt: new Date().toISOString()
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Find mod in config by slug, name, or project ID
122
+ * @param {Array} mods - Array of mod objects
123
+ * @param {string} query - Search query
124
+ * @returns {object|undefined} - Found mod or undefined
125
+ */
126
+ export function findMod(mods, query) {
127
+ const lowerQuery = query.toLowerCase();
128
+ return mods.find(m =>
129
+ m.slug.toLowerCase() === lowerQuery ||
130
+ m.name.toLowerCase() === lowerQuery ||
131
+ m.projectId === query
132
+ );
133
+ }
134
+
135
+ export default {
136
+ getProject,
137
+ getProjectVersions,
138
+ searchMods,
139
+ downloadMod,
140
+ findMod
141
+ };