mirror of
https://git.bits.team/Bits/mod-manager.git
synced 2024-12-21 20:08:21 -05:00
Download dependencies as well as the requested mod and track them
This commit is contained in:
parent
7ad2992149
commit
a820b9ce57
8
README.md
Normal file
8
README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Mod Manager
|
||||
A package manager-like CLI utility for installing, upgrading and migrating mods on Fabric Minecraft Servers
|
||||
|
||||
## Envrionment Variables
|
||||
The list of required variables are as follows:
|
||||
```
|
||||
CURSEFORGE_API_KEY="**api key goes here**"
|
||||
```
|
@ -14,7 +14,8 @@ import {readFileSync, unlinkSync} from "fs";
|
||||
import UpdateCommand from "./commands/upgrade_command.js";
|
||||
import MigratePossibleCommand from "./commands/migrate_possible.js";
|
||||
import MigrateCommand from "./commands/migrate_command.js";
|
||||
|
||||
import ModrinthSource from "./mods/sources/modrinth_source.js";
|
||||
import Mods from "./mods/mods.js";
|
||||
|
||||
export default class ModManager {
|
||||
public static logger: Logger | null = null;
|
||||
@ -41,7 +42,7 @@ export default class ModManager {
|
||||
public static readonly MODS_FOLDER_PATH = path.join("mods")
|
||||
}
|
||||
|
||||
static init() {
|
||||
static async init() {
|
||||
if (Initialiser.isInitialised()) {
|
||||
this.logger = ModManager.createLogger();
|
||||
}
|
||||
@ -54,6 +55,12 @@ export default class ModManager {
|
||||
command.registerCommand(this.program);
|
||||
}
|
||||
|
||||
/* const source = new CurseforgeSource();
|
||||
console.log(await source.search("lithium"))*/
|
||||
|
||||
Mods.registerSource(new ModrinthSource())
|
||||
//Mods.registerSource(new CurseforgeSource(), "CURSEFORGE_API_KEY")
|
||||
|
||||
this.program.showSuggestionAfterError();
|
||||
this.program.showHelpAfterError();
|
||||
this.program.parse();
|
||||
|
7
src/mods/mod.d.ts
vendored
7
src/mods/mod.d.ts
vendored
@ -6,14 +6,15 @@ declare global {
|
||||
fileName: string,
|
||||
version: string
|
||||
source: string,
|
||||
essential: boolean
|
||||
essential: boolean,
|
||||
}
|
||||
|
||||
type Version = {
|
||||
id: string
|
||||
modId: string
|
||||
fileName: string,
|
||||
url: string
|
||||
version_number: string
|
||||
versionNumber: string,
|
||||
dependencies: Array<Version>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import path from "path";
|
||||
import PrintUtils from "../util/print_utils.js";
|
||||
import ModrinthSource from "./sources/modrinth_source.js";
|
||||
import ModSource from "./sources/mod_source.js";
|
||||
import ModNotFoundError from "../errors/mod_not_found_error.js";
|
||||
import {readFileSync, unlinkSync, writeFileSync} from "fs";
|
||||
@ -10,9 +9,17 @@ import MinecraftUtils from "../util/minecraft_utils.js";
|
||||
import MigrateError from "../errors/migrate_error.js";
|
||||
|
||||
export default class Mods {
|
||||
private static readonly MOD_SOURCES: Array<ModSource> = [
|
||||
new ModrinthSource()
|
||||
];
|
||||
private static readonly MOD_SOURCES: Array<ModSource> = [];
|
||||
|
||||
public static registerSource(source: ModSource, envVar?: string) {
|
||||
if (envVar != undefined) {
|
||||
if (!process.env.hasOwnProperty(envVar)) {
|
||||
PrintUtils.warn(`${source.getSourceName()} could not be registered as a mod source, as the required environment variable ${envVar} was not detected. Functionality related to ${source.getSourceName()} will be skipped.`)
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.MOD_SOURCES.push(source);
|
||||
}
|
||||
|
||||
public static async install(mod: string, essential: boolean): Promise<void> {
|
||||
let success: boolean = false;
|
||||
@ -48,8 +55,7 @@ export default class Mods {
|
||||
try {
|
||||
const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion();
|
||||
const latestVersion = await source.getLatestVersion(id, mcVersion)
|
||||
const modObj: Mod = await source.install(latestVersion, essential);
|
||||
this.trackMod(modObj);
|
||||
await source.install(latestVersion, essential);
|
||||
|
||||
spinner.succeed(`Successfully installed ${projectName}`);
|
||||
success = true;
|
||||
@ -65,7 +71,7 @@ export default class Mods {
|
||||
}
|
||||
}
|
||||
|
||||
private static trackMod(mod: Mod): void {
|
||||
public static trackMod(mod: Mod): void {
|
||||
// Read current file
|
||||
const mods = this.getTrackedMods();
|
||||
|
||||
@ -181,14 +187,13 @@ export default class Mods {
|
||||
latestVersion = await source.getLatestVersion(mod.id, mcVersion);
|
||||
|
||||
// If the latest version has a different version number, it must be newer, install it.
|
||||
if (latestVersion.version_number != mod.version) {
|
||||
if (latestVersion.versionNumber != mod.version) {
|
||||
spinner.updateText(`Newer version for ${mod.name} found. Installing...`)
|
||||
this.silentUninstall(mod);
|
||||
|
||||
const newMod = await source.install(latestVersion, mod.essential);
|
||||
this.trackMod(newMod);
|
||||
await source.install(latestVersion, mod.essential);
|
||||
|
||||
spinner.succeed(`Successfully updated ${newMod.name}`)
|
||||
spinner.succeed(`Successfully updated ${mod.name}`)
|
||||
|
||||
// Else, the latest version is already installed, do nothing.
|
||||
} else {
|
||||
@ -300,11 +305,10 @@ export default class Mods {
|
||||
const latestVersion = await source.getLatestVersion(mod.id, version)
|
||||
|
||||
// Install the new mod
|
||||
spinner.updateText(`Installing ${mod.name} ${latestVersion.version_number}..`)
|
||||
const newMod = await source.install(latestVersion, mod.essential)
|
||||
this.trackMod(newMod)
|
||||
spinner.updateText(`Installing ${mod.name} ${latestVersion.versionNumber}..`)
|
||||
await source.install(latestVersion, mod.essential)
|
||||
|
||||
spinner.succeed(`Successfully installed ${newMod.name} ${newMod.version}`)
|
||||
spinner.succeed(`Successfully installed ${mod.name} ${mod.version}`)
|
||||
} catch (e) {
|
||||
// If a mod is not available, but is essential, throw error, else, warn user, and continue.
|
||||
if (mod.essential) {
|
||||
|
68
src/mods/sources/curseforge_source.ts
Normal file
68
src/mods/sources/curseforge_source.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/*import ModSource from "./mod_source.js";
|
||||
import axios from "axios";
|
||||
import MinecraftUtils from "../../util/minecraft_utils.js";
|
||||
import ModNotFoundError from "../../errors/mod_not_found_error.js";
|
||||
|
||||
export class CurseforgeSource implements ModSource {
|
||||
private static readonly BASE_URL: string = "https://api.curseforge.com";
|
||||
private static readonly SEARCH_URL: string = `${CurseforgeSource.BASE_URL}/v1/mods/search`;
|
||||
|
||||
private static readonly MINECRAFT_ID: number = 432;
|
||||
private static readonly FABRIC_TYPE: number = 4;
|
||||
|
||||
getLatestVersion(id: string, mcVersion: string): Promise<Version> {
|
||||
const response = await
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
getProjectName(id: string): Promise<string> {
|
||||
return Promise.resolve("");
|
||||
}
|
||||
|
||||
getSourceName(): string {
|
||||
return "Curseforge";
|
||||
}
|
||||
|
||||
install(version: Version, essential: boolean): Promise<Mod> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async search(query: string): Promise<string> {
|
||||
const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion();
|
||||
|
||||
const params = {
|
||||
gameId: CurseforgeSource.MINECRAFT_ID,
|
||||
gameVersion: mcVersion,
|
||||
modLoaderType: CurseforgeSource.FABRIC_TYPE,
|
||||
pageSize: 1,
|
||||
searchFilter: query
|
||||
}
|
||||
|
||||
const response = await this.makeRequest(CurseforgeSource.SEARCH_URL, params);
|
||||
|
||||
const id = response.data[0].id;
|
||||
|
||||
if (id == undefined) {
|
||||
throw new ModNotFoundError(`Mod ${query} could not be found on ${this.getSourceName()}`);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
private async makeRequest(url: string, params: object) {
|
||||
if (process.env.CURSEFORGE_API_KEY == undefined) {
|
||||
throw new Error("Attempted Curseforge api calls with undefined api key environment variable (CURSEFORGE_API_KEY)")
|
||||
}
|
||||
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
"x-api-key": process.env.CURSEFORGE_API_KEY
|
||||
},
|
||||
params
|
||||
})
|
||||
return await response.data;
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
@ -1,7 +1,7 @@
|
||||
export default interface ModSource {
|
||||
search(query: string): Promise<string>;
|
||||
|
||||
install(version: Version, essential: boolean): Promise<Mod>;
|
||||
install(version: Version, essential: boolean): Promise<void>;
|
||||
|
||||
getSourceName(): string;
|
||||
|
||||
|
@ -7,12 +7,14 @@ import ModNotFoundError from "../../errors/mod_not_found_error.js";
|
||||
import Util from "../../util/util.js";
|
||||
import FileDownloader from "../../io/file_downloder.js";
|
||||
import DownloadError from "../../errors/download_error.js";
|
||||
import Mods from "../mods.js";
|
||||
|
||||
export default class ModrinthSource implements ModSource {
|
||||
private static readonly BASE_URL: string = "https://api.modrinth.com/v2";
|
||||
private static readonly SEARCH_URL: string = ModrinthSource.BASE_URL + "/search";
|
||||
private static readonly LIST_VERSIONS_URL: string = ModrinthSource.BASE_URL + "/project/%s/version";
|
||||
private static readonly PROJECT_URL: string = ModrinthSource.BASE_URL + "/project/%s";
|
||||
private static readonly SINGLE_VERSION_URL: string = `${ModrinthSource.BASE_URL}/version/%s`;
|
||||
|
||||
/**
|
||||
* Searches Modrinth for the specified query
|
||||
@ -92,21 +94,27 @@ export default class ModrinthSource implements ModSource {
|
||||
* @param essential whether this mod is essential or not
|
||||
* @throws DownloadError if an error occurs when downloading
|
||||
*/
|
||||
async install(version: Version, essential: boolean): Promise<Mod> {
|
||||
async install(version: Version, essential: boolean): Promise<void> {
|
||||
try {
|
||||
if (!Util.isArrayEmpty(version.dependencies)) {
|
||||
for (let dependency of version.dependencies) {
|
||||
await this.install(dependency, essential);
|
||||
}
|
||||
}
|
||||
FileDownloader.downloadMod(version)
|
||||
|
||||
return {
|
||||
name: await this.getProjectName(version.id),
|
||||
id: version.id,
|
||||
const mod = {
|
||||
name: await this.getProjectName(version.modId),
|
||||
id: version.modId,
|
||||
fileName: version.fileName,
|
||||
version: version.version_number,
|
||||
version: version.versionNumber,
|
||||
source: this.getSourceName(),
|
||||
essential: essential
|
||||
};
|
||||
essential: essential,
|
||||
}
|
||||
|
||||
Mods.trackMod(mod);
|
||||
} catch (e) {
|
||||
throw new DownloadError(`An error occurred downloading mod with id ${version.id} from ${this.getSourceName()}`)
|
||||
throw new DownloadError(`An error occurred downloading mod with id ${version.modId} from ${this.getSourceName()}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -237,13 +245,67 @@ export default class ModrinthSource implements ModSource {
|
||||
throw new ModNotFoundError(`Mod with id ${id} has no available versions on ${this.getSourceName()} for Minecraft version ${mcVersion}`);
|
||||
}
|
||||
const latestVersion = results[0];
|
||||
const latestFile = results[0].files[0];
|
||||
|
||||
return this.getVersionFromId(latestVersion.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a version object from the provided version id
|
||||
* Example response from query:
|
||||
* {
|
||||
* "id": "3KmOcp6b",
|
||||
* "project_id": "P7dR8mSH",
|
||||
* "author_id": "JZA4dW8o",
|
||||
* "featured": false,
|
||||
* "name": "[1.19] Fabric API 0.58.0+1.19",
|
||||
* "version_number": "0.58.0+1.19",
|
||||
* "changelog": "- Bump version (modmuss50)\n- Enable parallel builds by default. Update remotesign to a parallel capable version. Set org.gradle.parallel.threads in actions as we are IO bound. (modmuss50)\n- fix custom dimension not loaded on world preset other than default (#2387) (deirn)\n- Fix inconsistent ordering of item attribute modifiers by using a linked hashmap (#2380) (Technici4n)\n- Fix incorrect check in GlobalReceiverRegistry (#2363) (apple502j)\n- Make disconnected screen reason text scrollable (#2349) (deirn, modmuss50)\n- Fix Indigo AO calculation (#2344) (PepperCode1)\n",
|
||||
* "changelog_url": null,
|
||||
* "date_published": "2022-07-21T20:10:41.654884Z",
|
||||
* "downloads": 16745,
|
||||
* "version_type": "release",
|
||||
* "files": [
|
||||
* {
|
||||
* "hashes": {
|
||||
* "sha512": "9c948488852e3bcf7a84fef26465bf0bcfbba17fb03e6b56ae11cf82d1ae6abbfb4c569bf3f1d088c6c3c5219d37c2699afc9013926f588263210a19f8d6e235",
|
||||
* "sha1": "6d29acc99b293b2be7060df6d7c887812bd54e46"
|
||||
* },
|
||||
* "url": "https://cdn.modrinth.com/data/P7dR8mSH/versions/0.58.0+1.19/fabric-api-0.58.0%2B1.19.jar",
|
||||
* "filename": "fabric-api-0.58.0+1.19.jar",
|
||||
* "primary": false,
|
||||
* "size": 1496048
|
||||
* }
|
||||
* ],
|
||||
* "dependencies": [],
|
||||
* "game_versions": [
|
||||
* "1.19"
|
||||
* ],
|
||||
* "loaders": [
|
||||
* "fabric"
|
||||
* ]
|
||||
* }
|
||||
* @param versionId the version id to transform into an object
|
||||
* @return the Version object
|
||||
*/
|
||||
async getVersionFromId(versionId: string): Promise<Version> {
|
||||
const response = await axios.get(format(ModrinthSource.SINGLE_VERSION_URL, versionId));
|
||||
const latestVersion = await response.data;
|
||||
|
||||
const latestFile = latestVersion.files[0];
|
||||
|
||||
const dependencies = [];
|
||||
if (!Util.isArrayEmpty(latestVersion.dependencies)) {
|
||||
for (let dependency of latestVersion.dependencies) {
|
||||
dependencies.push(await this.getVersionFromId(dependency.version_id))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: latestVersion.project_id,
|
||||
version_number: latestVersion.version_number,
|
||||
modId: latestVersion.project_id,
|
||||
versionNumber: latestVersion.version_number,
|
||||
fileName: latestFile.filename,
|
||||
url: latestFile.url
|
||||
url: latestFile.url,
|
||||
dependencies: dependencies
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user