diff --git a/package-lock.json b/package-lock.json index fa80d02..b41f9d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "commander": "^9.4.0", "ora": "^6.1.2", "pino": "^8.3.1", - "string-format": "^2.0.0" + "string-format": "^2.0.0", + "string-similarity-js": "^2.1.4" }, "devDependencies": { "@types/node": "^18.6.3", @@ -546,6 +547,11 @@ "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==" }, + "node_modules/string-similarity-js": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/string-similarity-js/-/string-similarity-js-2.1.4.tgz", + "integrity": "sha512-uApODZNjCHGYROzDSAdCmAHf60L/pMDHnP/yk6TAbvGg7JSPZlSto/ceCI7hZEqzc53/juU2aOJFkM2yUVTMTA==" + }, "node_modules/strip-ansi": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", @@ -946,6 +952,11 @@ "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==" }, + "string-similarity-js": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/string-similarity-js/-/string-similarity-js-2.1.4.tgz", + "integrity": "sha512-uApODZNjCHGYROzDSAdCmAHf60L/pMDHnP/yk6TAbvGg7JSPZlSto/ceCI7hZEqzc53/juU2aOJFkM2yUVTMTA==" + }, "strip-ansi": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", diff --git a/package.json b/package.json index f6094e6..b08d752 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "commander": "^9.4.0", "ora": "^6.1.2", "pino": "^8.3.1", - "string-format": "^2.0.0" + "string-format": "^2.0.0", + "string-similarity-js": "^2.1.4" }, "devDependencies": { "@types/node": "^18.6.3", diff --git a/src/commands/essential_command.ts b/src/commands/essential_command.ts new file mode 100644 index 0000000..4450a3b --- /dev/null +++ b/src/commands/essential_command.ts @@ -0,0 +1,20 @@ +import Subcommand from "./subcommand.js"; +import {Command} from "commander"; +import ModManager from "../mod-manager.js"; +import Mods from "../mods/mods.js"; + +export default class EssentialCommand implements Subcommand { + registerCommand(program: Command): void { + program.command("essential") + .description("Marks mods as essential") + .argument("", "The mods to mark as essential (as names or ids)") + .action((mods) => { + ModManager.execute(() => { + for (let mod of mods) { + Mods.markEssential(mod) + } + }) + }) + } + +} \ No newline at end of file diff --git a/src/commands/install_command.ts b/src/commands/install_command.ts index 4d9ffb3..3f31a8a 100644 --- a/src/commands/install_command.ts +++ b/src/commands/install_command.ts @@ -8,10 +8,11 @@ export default class InstallCommand implements Subcommand { program.command("install") .description("Installs the provided mods") .argument("", "The mods to install") - .action((mods) => { + .option("-e, --essential", "Marks these mods as essential", false) + .action(function() { ModManager.execute(async () => { - for (const mod of mods) { - await Mods.install(mod); + for (const mod of this.args) { + await Mods.install(mod, this.opts().essential); } }) }); diff --git a/src/commands/uninstall_command.ts b/src/commands/uninstall_command.ts index 51fca6f..ed1566d 100644 --- a/src/commands/uninstall_command.ts +++ b/src/commands/uninstall_command.ts @@ -7,7 +7,7 @@ export default class UninstallCommand implements Subcommand { registerCommand(program: Command): void { program.command("uninstall") .description("Uninstalls the provided mods") - .argument("") + .argument("", "The mods to uninstall (as names or ids)") .action((mods) => { ModManager.execute(() => { for (let mod of mods) { diff --git a/src/mod-manager.ts b/src/mod-manager.ts index 7b35610..7601565 100644 --- a/src/mod-manager.ts +++ b/src/mod-manager.ts @@ -9,6 +9,7 @@ import path from "path"; import {Logger, pino} from "pino" import {ListCommand} from "./commands/list_command.js"; import UninstallCommand from "./commands/uninstall_command.js"; +import EssentialCommand from "./commands/essential_command.js"; export default class ModManager { @@ -21,7 +22,8 @@ export default class ModManager { new InitCommand(), new InstallCommand(), new ListCommand(), - new UninstallCommand() + new UninstallCommand(), + new EssentialCommand() ]; static init() { diff --git a/src/mods/mod.d.ts b/src/mods/mod.d.ts index 282b553..f2cef86 100644 --- a/src/mods/mod.d.ts +++ b/src/mods/mod.d.ts @@ -1,10 +1,12 @@ declare global { + // DONT FORGET TO UPDATE CONSTRUCTORS WHEN MOD SIGNATURE CHANGES type Mod = { id: string name: string fileName: string, version: string source: string, + essential: boolean } } diff --git a/src/mods/mods.ts b/src/mods/mods.ts index 82e383c..4bcedb2 100644 --- a/src/mods/mods.ts +++ b/src/mods/mods.ts @@ -15,7 +15,7 @@ export default class Mods { new ModrinthSource() ]; - public static async install(mod: string): Promise { + public static async install(mod: string, essential: boolean): Promise { let success: boolean = false; // Go through each mod source @@ -47,7 +47,7 @@ export default class Mods { if (!this.isModInstalled(id)) { spinner.updateText(`Installing ${projectName}...`) try { - const modObj: Mod = await source.install(id); + const modObj: Mod = await source.install(id, essential); this.trackMod(modObj); spinner.succeed(`Successfully installed ${projectName}`); @@ -94,30 +94,17 @@ export default class Mods { } static uninstall(mod: string) { - let mods: Array = this.getTrackedMods(); - - // Replace underscores with spaces - mod = mod.replaceAll("_", " "); - // Find mod to uninstall const spinner = new PrintUtils.Spinner(`Uninstalling ${mod}...`) - let modToUninstall: Mod | undefined = undefined; - for (let modEle of mods) { - const id = modEle.id.toLowerCase(); - const name = modEle.name.toLowerCase(); - - const query = mod.toLowerCase(); - if (id == query || name == query) { - modToUninstall = modEle; - break; - } - } + 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(this.MODS_FOLDER_PATH, modToUninstall.fileName)); - mods = mods.filter(item => item !== modToUninstall); + mods = mods.filter(item => !Mods.areModsEqual(item, modToUninstall)); this.writeFile(mods); spinner.succeed(`${modToUninstall.name} successfully uninstalled!`) } else { @@ -127,5 +114,46 @@ export default class Mods { + } + static areModsEqual(mod1: Mod, mod2: Mod): boolean { + return mod1.id === mod2.id; + } + + static markEssential(mod: string) { + const modToMark = this.findMod(mod); + + if (modToMark != undefined) { + let mods = this.getTrackedMods(); + // Remove mod from list + mods = mods.filter(item => !Mods.areModsEqual(item, modToMark)); + + // Mark is as essential, and read it + modToMark.essential = true; + mods.push(modToMark) + + this.writeFile(mods); + + PrintUtils.success(`Marked ${modToMark.name} as essential`) + } else { + PrintUtils.error(`${mod} not found.`) + } + } + + private static findMod(mod: string): Mod | undefined { + // Replace underscores with spaces + mod = mod.replaceAll("_", " "); + + let mods: Array = this.getTrackedMods(); + for (let modEle of mods) { + const id = modEle.id.toLowerCase(); + const name = modEle.name.toLowerCase(); + + const query = mod.toLowerCase(); + if (id == query || Util.areStringsSimilar(mod, name)) { + return modEle; + } + } + + return undefined; } } \ No newline at end of file diff --git a/src/mods/sources/mod_source.ts b/src/mods/sources/mod_source.ts index 6e95a61..3c3dada 100644 --- a/src/mods/sources/mod_source.ts +++ b/src/mods/sources/mod_source.ts @@ -1,7 +1,7 @@ export default interface ModSource { search(query: string): Promise; - install(id: string): Promise; + install(id: string, essential: boolean): Promise; getSourceName(): string; diff --git a/src/mods/sources/modrinth_source.ts b/src/mods/sources/modrinth_source.ts index d8e158b..a8ad6a4 100644 --- a/src/mods/sources/modrinth_source.ts +++ b/src/mods/sources/modrinth_source.ts @@ -124,10 +124,11 @@ export default class ModrinthSource implements ModSource { * } * ] * @param id the id of the mod + * @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): Promise { + async install(id: string, essential: boolean): Promise { const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion(); const params = { @@ -161,7 +162,8 @@ export default class ModrinthSource implements ModSource { id: id, fileName: fileName, version: modVersion, - source: this.getSourceName() + source: this.getSourceName(), + essential: essential }; } catch (e) { diff --git a/src/util/util.ts b/src/util/util.ts index a2dbae3..b1cd784 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,4 +1,8 @@ +import { stringSimilarity } from "string-similarity-js"; + export default class Util { + private static readonly SIMILARITY_THRESHOLD: number = 0.8; + static isArrayEmpty(array: Array | undefined): boolean { return array === undefined || array.length == 0; } @@ -9,4 +13,10 @@ export default class Util { // uppercase the first character .replace(/^./, function(str){ return str.toUpperCase(); }) } + + static areStringsSimilar(master: string, compare: string): boolean { + master = master.toLowerCase(); + compare = compare.toLowerCase(); + return stringSimilarity(master, compare) >= this.SIMILARITY_THRESHOLD; + } }