mirror of
https://git.bits.team/Bits/mod-manager.git
synced 2024-11-14 10:28: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 UpdateCommand from "./commands/upgrade_command.js";
|
||||||
import MigratePossibleCommand from "./commands/migrate_possible.js";
|
import MigratePossibleCommand from "./commands/migrate_possible.js";
|
||||||
import MigrateCommand from "./commands/migrate_command.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 {
|
export default class ModManager {
|
||||||
public static logger: Logger | null = null;
|
public static logger: Logger | null = null;
|
||||||
@ -41,7 +42,7 @@ export default class ModManager {
|
|||||||
public static readonly MODS_FOLDER_PATH = path.join("mods")
|
public static readonly MODS_FOLDER_PATH = path.join("mods")
|
||||||
}
|
}
|
||||||
|
|
||||||
static init() {
|
static async init() {
|
||||||
if (Initialiser.isInitialised()) {
|
if (Initialiser.isInitialised()) {
|
||||||
this.logger = ModManager.createLogger();
|
this.logger = ModManager.createLogger();
|
||||||
}
|
}
|
||||||
@ -54,6 +55,12 @@ export default class ModManager {
|
|||||||
command.registerCommand(this.program);
|
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.showSuggestionAfterError();
|
||||||
this.program.showHelpAfterError();
|
this.program.showHelpAfterError();
|
||||||
this.program.parse();
|
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,
|
fileName: string,
|
||||||
version: string
|
version: string
|
||||||
source: string,
|
source: string,
|
||||||
essential: boolean
|
essential: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Version = {
|
type Version = {
|
||||||
id: string
|
modId: string
|
||||||
fileName: string,
|
fileName: string,
|
||||||
url: string
|
url: string
|
||||||
version_number: string
|
versionNumber: string,
|
||||||
|
dependencies: Array<Version>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import PrintUtils from "../util/print_utils.js";
|
import PrintUtils from "../util/print_utils.js";
|
||||||
import ModrinthSource from "./sources/modrinth_source.js";
|
|
||||||
import ModSource from "./sources/mod_source.js";
|
import ModSource from "./sources/mod_source.js";
|
||||||
import ModNotFoundError from "../errors/mod_not_found_error.js";
|
import ModNotFoundError from "../errors/mod_not_found_error.js";
|
||||||
import {readFileSync, unlinkSync, writeFileSync} from "fs";
|
import {readFileSync, unlinkSync, writeFileSync} from "fs";
|
||||||
@ -10,9 +9,17 @@ import MinecraftUtils from "../util/minecraft_utils.js";
|
|||||||
import MigrateError from "../errors/migrate_error.js";
|
import MigrateError from "../errors/migrate_error.js";
|
||||||
|
|
||||||
export default class Mods {
|
export default class Mods {
|
||||||
private static readonly MOD_SOURCES: Array<ModSource> = [
|
private static readonly MOD_SOURCES: Array<ModSource> = [];
|
||||||
new ModrinthSource()
|
|
||||||
];
|
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> {
|
public static async install(mod: string, essential: boolean): Promise<void> {
|
||||||
let success: boolean = false;
|
let success: boolean = false;
|
||||||
@ -48,8 +55,7 @@ export default class Mods {
|
|||||||
try {
|
try {
|
||||||
const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion();
|
const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion();
|
||||||
const latestVersion = await source.getLatestVersion(id, mcVersion)
|
const latestVersion = await source.getLatestVersion(id, mcVersion)
|
||||||
const modObj: Mod = await source.install(latestVersion, essential);
|
await source.install(latestVersion, essential);
|
||||||
this.trackMod(modObj);
|
|
||||||
|
|
||||||
spinner.succeed(`Successfully installed ${projectName}`);
|
spinner.succeed(`Successfully installed ${projectName}`);
|
||||||
success = true;
|
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
|
// Read current file
|
||||||
const mods = this.getTrackedMods();
|
const mods = this.getTrackedMods();
|
||||||
|
|
||||||
@ -181,14 +187,13 @@ export default class Mods {
|
|||||||
latestVersion = await source.getLatestVersion(mod.id, mcVersion);
|
latestVersion = await source.getLatestVersion(mod.id, mcVersion);
|
||||||
|
|
||||||
// If the latest version has a different version number, it must be newer, install it.
|
// 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...`)
|
spinner.updateText(`Newer version for ${mod.name} found. Installing...`)
|
||||||
this.silentUninstall(mod);
|
this.silentUninstall(mod);
|
||||||
|
|
||||||
const newMod = await source.install(latestVersion, mod.essential);
|
await source.install(latestVersion, mod.essential);
|
||||||
this.trackMod(newMod);
|
|
||||||
|
|
||||||
spinner.succeed(`Successfully updated ${newMod.name}`)
|
spinner.succeed(`Successfully updated ${mod.name}`)
|
||||||
|
|
||||||
// Else, the latest version is already installed, do nothing.
|
// Else, the latest version is already installed, do nothing.
|
||||||
} else {
|
} else {
|
||||||
@ -300,11 +305,10 @@ export default class Mods {
|
|||||||
const latestVersion = await source.getLatestVersion(mod.id, version)
|
const latestVersion = await source.getLatestVersion(mod.id, version)
|
||||||
|
|
||||||
// Install the new mod
|
// Install the new mod
|
||||||
spinner.updateText(`Installing ${mod.name} ${latestVersion.version_number}..`)
|
spinner.updateText(`Installing ${mod.name} ${latestVersion.versionNumber}..`)
|
||||||
const newMod = await source.install(latestVersion, mod.essential)
|
await source.install(latestVersion, mod.essential)
|
||||||
this.trackMod(newMod)
|
|
||||||
|
|
||||||
spinner.succeed(`Successfully installed ${newMod.name} ${newMod.version}`)
|
spinner.succeed(`Successfully installed ${mod.name} ${mod.version}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If a mod is not available, but is essential, throw error, else, warn user, and continue.
|
// If a mod is not available, but is essential, throw error, else, warn user, and continue.
|
||||||
if (mod.essential) {
|
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 {
|
export default interface ModSource {
|
||||||
search(query: string): Promise<string>;
|
search(query: string): Promise<string>;
|
||||||
|
|
||||||
install(version: Version, essential: boolean): Promise<Mod>;
|
install(version: Version, essential: boolean): Promise<void>;
|
||||||
|
|
||||||
getSourceName(): string;
|
getSourceName(): string;
|
||||||
|
|
||||||
|
@ -7,12 +7,14 @@ import ModNotFoundError from "../../errors/mod_not_found_error.js";
|
|||||||
import Util from "../../util/util.js";
|
import Util from "../../util/util.js";
|
||||||
import FileDownloader from "../../io/file_downloder.js";
|
import FileDownloader from "../../io/file_downloder.js";
|
||||||
import DownloadError from "../../errors/download_error.js";
|
import DownloadError from "../../errors/download_error.js";
|
||||||
|
import Mods from "../mods.js";
|
||||||
|
|
||||||
export default class ModrinthSource implements ModSource {
|
export default class ModrinthSource implements ModSource {
|
||||||
private static readonly BASE_URL: string = "https://api.modrinth.com/v2";
|
private static readonly BASE_URL: string = "https://api.modrinth.com/v2";
|
||||||
private static readonly SEARCH_URL: string = ModrinthSource.BASE_URL + "/search";
|
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 LIST_VERSIONS_URL: string = ModrinthSource.BASE_URL + "/project/%s/version";
|
||||||
private static readonly PROJECT_URL: string = ModrinthSource.BASE_URL + "/project/%s";
|
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
|
* 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
|
* @param essential whether this mod is essential or not
|
||||||
* @throws DownloadError if an error occurs when downloading
|
* @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 {
|
try {
|
||||||
|
if (!Util.isArrayEmpty(version.dependencies)) {
|
||||||
|
for (let dependency of version.dependencies) {
|
||||||
|
await this.install(dependency, essential);
|
||||||
|
}
|
||||||
|
}
|
||||||
FileDownloader.downloadMod(version)
|
FileDownloader.downloadMod(version)
|
||||||
|
|
||||||
return {
|
const mod = {
|
||||||
name: await this.getProjectName(version.id),
|
name: await this.getProjectName(version.modId),
|
||||||
id: version.id,
|
id: version.modId,
|
||||||
fileName: version.fileName,
|
fileName: version.fileName,
|
||||||
version: version.version_number,
|
version: version.versionNumber,
|
||||||
source: this.getSourceName(),
|
source: this.getSourceName(),
|
||||||
essential: essential
|
essential: essential,
|
||||||
};
|
}
|
||||||
|
|
||||||
|
Mods.trackMod(mod);
|
||||||
} catch (e) {
|
} 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}`);
|
throw new ModNotFoundError(`Mod with id ${id} has no available versions on ${this.getSourceName()} for Minecraft version ${mcVersion}`);
|
||||||
}
|
}
|
||||||
const latestVersion = results[0];
|
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 {
|
return {
|
||||||
id: latestVersion.project_id,
|
modId: latestVersion.project_id,
|
||||||
version_number: latestVersion.version_number,
|
versionNumber: latestVersion.version_number,
|
||||||
fileName: latestFile.filename,
|
fileName: latestFile.filename,
|
||||||
url: latestFile.url
|
url: latestFile.url,
|
||||||
|
dependencies: dependencies
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user