diff --git a/src/commands/upgrade_command.ts b/src/commands/upgrade_command.ts new file mode 100644 index 0000000..92bddf2 --- /dev/null +++ b/src/commands/upgrade_command.ts @@ -0,0 +1,17 @@ +import Subcommand from "./subcommand.js"; +import {Command} from "commander"; +import ModManager from "../mod-manager.js"; +import Mods from "../mods/mods.js"; + +export default class UpgradeCommand implements Subcommand { + registerCommand(program: Command): void { + program.command("update") + .description("Checks for and updates mods that have a newer available version") + .action(() => { + ModManager.execute(async() => { + await Mods.update(); + }) + }) + } + +} \ No newline at end of file diff --git a/src/io/download_task.d.ts b/src/io/download_task.d.ts deleted file mode 100644 index 074148e..0000000 --- a/src/io/download_task.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare global { - type DownloadTask = { - fileName: string, - url: string - } -} - -export {} \ No newline at end of file diff --git a/src/io/file_downloder.ts b/src/io/file_downloder.ts index a0e11ca..2fec8eb 100644 --- a/src/io/file_downloder.ts +++ b/src/io/file_downloder.ts @@ -6,14 +6,14 @@ import ModManager from "../mod-manager.js"; export default class FileDownloader { - static downloadMod(task: DownloadTask): void { - https.get(task.url, res => { - const filePath = path.join(ModManager.FilePaths.MODS_FOLDER_PATH, task.fileName); + static downloadMod(version: Version): void { + https.get(version.url, res => { + const filePath = path.join(ModManager.FilePaths.MODS_FOLDER_PATH, version.fileName); const writeStream = createWriteStream(filePath); res.pipe(writeStream); writeStream.on("finish", () => writeStream.close()); writeStream.on('error', () => { - throw new DownloadError(`Failed to download ${task.fileName} from ${task.url}`) + throw new DownloadError(`Failed to download ${version.fileName} from ${version.url}`) }) }) } diff --git a/src/mod-manager.ts b/src/mod-manager.ts index cde7c26..ab34800 100644 --- a/src/mod-manager.ts +++ b/src/mod-manager.ts @@ -11,6 +11,7 @@ import {ListCommand} from "./commands/list_command.js"; import UninstallCommand from "./commands/uninstall_command.js"; import EssentialCommand from "./commands/essential_command.js"; import {readFileSync, unlinkSync} from "fs"; +import UpgradeCommand from "./commands/upgrade_command.js"; export default class ModManager { @@ -23,7 +24,8 @@ export default class ModManager { new InstallCommand(), new ListCommand(), new UninstallCommand(), - new EssentialCommand() + new EssentialCommand(), + new UpgradeCommand() ]; static FilePaths = class { @@ -48,6 +50,8 @@ export default class ModManager { command.registerCommand(this.program); } + this.program.showSuggestionAfterError(); + this.program.showHelpAfterError(); this.program.parse(); } diff --git a/src/mods/mod.d.ts b/src/mods/mod.d.ts index f2cef86..8539d4f 100644 --- a/src/mods/mod.d.ts +++ b/src/mods/mod.d.ts @@ -8,6 +8,13 @@ declare global { source: string, essential: boolean } + + type Version = { + id: string + fileName: string, + url: string + version_number: string + } } export {} \ No newline at end of file diff --git a/src/mods/mods.ts b/src/mods/mods.ts index cf5d428..7c448ff 100644 --- a/src/mods/mods.ts +++ b/src/mods/mods.ts @@ -6,7 +6,7 @@ import ModNotFoundError from "../errors/mod_not_found_error.js"; import {readFileSync, unlinkSync, writeFileSync} from "fs"; import Util from "../util/util.js"; import ModManager from "../mod-manager.js"; - +import MinecraftUtils from "../util/minecraft_utils.js"; export default class Mods { private static readonly MOD_SOURCES: Array = [ @@ -45,7 +45,9 @@ export default class Mods { if (!this.isModInstalled(id)) { spinner.updateText(`Installing ${projectName}...`) try { - const modObj: Mod = await source.install(id, essential); + const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion(); + const latestVersion = await source.getLatestVersion(id, mcVersion) + const modObj: Mod = await source.install(latestVersion, essential); this.trackMod(modObj); spinner.succeed(`Successfully installed ${projectName}`); @@ -90,25 +92,27 @@ export default class Mods { static uninstall(mod: string) { // Find mod to uninstall const spinner = new PrintUtils.Spinner(`Uninstalling ${mod}...`) + spinner.start(); const modToUninstall = this.findMod(mod); // IF a matching mod is found, remove it if (modToUninstall != undefined) { - let mods: Array = this.getTrackedMods(); - - // Remove mod from list and uninstall it - unlinkSync(path.join(ModManager.FilePaths.MOD_FILE_PATH, modToUninstall.fileName)); - mods = mods.filter(item => !Mods.areModsEqual(item, modToUninstall)); - this.writeFile(mods); + this.silentUninstall(modToUninstall); spinner.succeed(`${modToUninstall.name} successfully uninstalled!`) } else { spinner.error(`${mod} was not found.`) } - - - - } + + static silentUninstall(mod: Mod) { + let mods: Array = this.getTrackedMods(); + + // Remove mod from list and uninstall it + unlinkSync(path.join(ModManager.FilePaths.MODS_FOLDER_PATH, mod.fileName)); + mods = mods.filter(item => !Mods.areModsEqual(item, mod)); + this.writeFile(mods); + } + static areModsEqual(mod1: Mod, mod2: Mod): boolean { return mod1.id === mod2.id; } @@ -153,4 +157,54 @@ export default class Mods { return undefined; } + + static async update() { + const trackedMods = this.getTrackedMods(); + + if (Util.isArrayEmpty(trackedMods)) { + PrintUtils.error("There are no mods currently installed. Try `mod-manager install -h` to learn more!") + return; + } + + const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion(); + + // For every tracked mod + for (let mod of trackedMods) { + const spinner = new PrintUtils.Spinner(`Checking for newer version of ${mod.name}`) + spinner.start(); + + // Get the latest version + const source = this.getSourceFromName(mod.source); + let latestVersion: Version | undefined = undefined; + try { + 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) { + spinner.updateText(`Newer version for ${mod.name} found. Installing...`) + this.silentUninstall(mod); + + const newMod = await source.install(latestVersion, mod.essential); + this.trackMod(newMod); + + spinner.succeed(`Successfully updated ${newMod.name}`) + + // Else, the latest version is already installed, do nothing. + } else { + throw new ModNotFoundError("There is no newer version available."); + } + } catch (e) { + spinner.error(`${mod.name} already has the latest version installed!`) + } + } + } + + static getSourceFromName(name: string): ModSource { + const source = this.MOD_SOURCES.filter(src => src.getSourceName() === name)[0]; + if (source == undefined) { + throw new Error(`There is no source registered with the name ${name}`) + } + + return source + } } \ No newline at end of file diff --git a/src/mods/sources/mod_source.ts b/src/mods/sources/mod_source.ts index 3c3dada..9402803 100644 --- a/src/mods/sources/mod_source.ts +++ b/src/mods/sources/mod_source.ts @@ -1,9 +1,11 @@ export default interface ModSource { search(query: string): Promise; - install(id: string, essential: boolean): Promise; + install(version: Version, essential: boolean): Promise; getSourceName(): string; getProjectName(id: string): Promise; + + getLatestVersion(id: string, mcVersion: string): Promise; } \ No newline at end of file diff --git a/src/mods/sources/modrinth_source.ts b/src/mods/sources/modrinth_source.ts index a8ad6a4..e0f26c0 100644 --- a/src/mods/sources/modrinth_source.ts +++ b/src/mods/sources/modrinth_source.ts @@ -11,7 +11,7 @@ import DownloadError from "../../errors/download_error.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 INSTALL_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"; /** @@ -87,87 +87,26 @@ export default class ModrinthSource implements ModSource { } /** - * Installs the mod with the provided mod id - * Example shape of data returned by query: - * [ - * { - * "id": "ZRR9yqHD", - * "project_id": "gvQqBUqZ", - * "author_id": "uhPSqlnd", - * "featured": false, - * "name": "Lithium 0.8.3", - * "version_number": "mc1.19.1-0.8.3", - * "changelog": "Lithium 0.8.3 is the second release for 1.19.1! It includes a bugfix too!\n\n## Fixes\n- fix: update chunk serialization patch to new mappings\n\nYou can donate on patreon: https://www.patreon.com/2No2Name\n", - * "changelog_url": null, - * "date_published": "2022-07-29T22:18:09.072973Z", - * "downloads": 3592, - * "version_type": "release", - * "files": [ - * { - * "hashes": { - * "sha1": "9ef9f10f62d4c19b736fe493f2a11d737fbe3d7c", - * "sha512": "a3b623b4c14f6ba46d1486ffb3d1ba3174e3317b419b2ddfdf7bb572244e706d2e0a37bdce169c94455bec00fd107530ba78d7e611162a632cc6950e6a625433" - * }, - * "url": "https://cdn.modrinth.com/data/gvQqBUqZ/versions/mc1.19.1-0.8.3/lithium-fabric-mc1.19.1-0.8.3.jar", - * "filename": "lithium-fabric-mc1.19.1-0.8.3.jar", - * "primary": true, - * "size": 476619 - * } - * ], - * "dependencies": [], - * "game_versions": [ - * "1.19.1" - * ], - * "loaders": [ - * "fabric" - * ] - * } - * ] - * @param id the id of the mod + * Installs the provided Version + * @param version the Version to install * @param essential whether this mod is essential or not * @throws DownloadError if an error occurs when downloading - * @throws ModNotFoundError if there are no versions available for the current Minecraft Version */ - async install(id: string, essential: boolean): Promise { - const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion(); - - const params = { - loaders: '["fabric"]', - game_versions: format('["%s"]', mcVersion) - } - - const response = await axios.get(format(ModrinthSource.INSTALL_URL, id), {params}); - const results = await response.data; - - if (Util.isArrayEmpty(results)) { - throw new ModNotFoundError(`Mod with id ${id} has no available versions on ${this.getSourceName()} for Minecraft version ${mcVersion}`); - } - - const latestFile = results[0].files[0]; - - const fileName = latestFile.filename; - const url = latestFile.url; - const modVersion = results[0].version_number; - - const task: DownloadTask = { - fileName: fileName, - url: url - } - + async install(version: Version, essential: boolean): Promise { try { - FileDownloader.downloadMod(task) + FileDownloader.downloadMod(version) return { - name: await this.getProjectName(id), - id: id, - fileName: fileName, - version: modVersion, + name: await this.getProjectName(version.id), + id: version.id, + fileName: version.fileName, + version: version.version_number, source: this.getSourceName(), essential: essential }; } catch (e) { - throw new DownloadError(`An error occurred downloading mod with id ${id} from ${this.getSourceName()}`) + throw new DownloadError(`An error occurred downloading mod with id ${version.id} from ${this.getSourceName()}`) } } @@ -244,4 +183,67 @@ export default class ModrinthSource implements ModSource { return await response.data.title; } + /** + * Gets the latest version of the mod + * Example shape of data returned by query: + * [ + * { + * "id": "ZRR9yqHD", + * "project_id": "gvQqBUqZ", + * "author_id": "uhPSqlnd", + * "featured": false, + * "name": "Lithium 0.8.3", + * "version_number": "mc1.19.1-0.8.3", + * "changelog": "Lithium 0.8.3 is the second release for 1.19.1! It includes a bugfix too!\n\n## Fixes\n- fix: update chunk serialization patch to new mappings\n\nYou can donate on patreon: https://www.patreon.com/2No2Name\n", + * "changelog_url": null, + * "date_published": "2022-07-29T22:18:09.072973Z", + * "downloads": 3592, + * "version_type": "release", + * "files": [ + * { + * "hashes": { + * "sha1": "9ef9f10f62d4c19b736fe493f2a11d737fbe3d7c", + * "sha512": "a3b623b4c14f6ba46d1486ffb3d1ba3174e3317b419b2ddfdf7bb572244e706d2e0a37bdce169c94455bec00fd107530ba78d7e611162a632cc6950e6a625433" + * }, + * "url": "https://cdn.modrinth.com/data/gvQqBUqZ/versions/mc1.19.1-0.8.3/lithium-fabric-mc1.19.1-0.8.3.jar", + * "filename": "lithium-fabric-mc1.19.1-0.8.3.jar", + * "primary": true, + * "size": 476619 + * } + * ], + * "dependencies": [], + * "game_versions": [ + * "1.19.1" + * ], + * "loaders": [ + * "fabric" + * ] + * } + * ] + * @param id + * @param mcVersion + * @throws ModNotFoundError if there are no versions available for the current Minecraft Version + */ + async getLatestVersion(id: string, mcVersion: string): Promise { + const params = { + loaders: '["fabric"]', + game_versions: format('["%s"]', mcVersion) + } + + const response = await axios.get(format(ModrinthSource.LIST_VERSIONS_URL, id), {params}); + const results = await response.data; + + if (Util.isArrayEmpty(results)) { + 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 { + id: latestVersion.project_id, + version_number: latestVersion.version_number, + fileName: latestFile.filename, + url: latestFile.url + }; + } } \ No newline at end of file