import { Command } from "commander" import kleur from "kleur" import { usePack } from "./pack.js" import loudRejection from "loud-rejection" import { modrinthCommand } from "./commands/modrinth.js" import { packwizCommand } from "./commands/packwiz.js" import dedent from "dedent" import { default as wrapAnsi } from "wrap-ansi" import { CURRENT_HORIZR_FILE_FORMAT_VERSION, HorizrFile, removeModFile } from "./files.js" import { output } from "./output.js" import figures from "figures" import { releaseChannelOrder } from "./shared.js" import fs from "fs-extra" import { Path } from "./path.js" import enquirer from "enquirer" import { clearCache } from "./utils.js" import { fetchFabricMinecraftVersions, fetchFabricVersions } from "./fabricApi.js" const program = new Command("horizr") .version( (await fs.readJson(Path.create(import.meta.url.slice(5)).getParent().resolve("../package.json").toString())).version, "-v, --version" ) .option("--clear-cache", "Clear the HTTP cache before doing the operation.") .on("option:clear-cache", () => { clearCache() output.println(kleur.green("Cache was cleared.\n")) }) program.command("init ") .description("Initialize a new pack in the directory.") .action(async path => { const directoryPath = Path.create(path) const horizrFilePath = directoryPath.resolve("horizr.json") if (await fs.pathExists(horizrFilePath.toString())) output.failAndExit(`${kleur.yellow("horizr.json")} already exists in the directory.`) await fs.mkdirp(directoryPath.toString()) const minecraftVersions = await output.withLoading(fetchFabricMinecraftVersions(), "Fetching Minecraft versions") const answers: any = await enquirer.prompt([ { name: "name", type: "input", message: "Name", validate: answer => answer.length === 0 ? "An answer is required." : true }, { name: "authors", type: "input", message: "Authors (comma-separated)", validate: answer => answer.length === 0 ? "An answer is required." : true }, { name: "description", type: "text", message: "Description" }, { name: "license", type: "text", message: "License (SPDX-ID)", validate: answer => answer.length === 0 ? "An answer is required." : true }, { name: "minecraftVersion", type: "autocomplete", message: "Minecraft version", choices: minecraftVersions.map(version => ({ name: version, value: version })), // @ts-expect-error limit: 10, validate: answer => minecraftVersions.includes(answer) ? true : "Please select a version from the list." } ]) const fabricVersion = (await output.withLoading(fetchFabricVersions(answers.minecraftVersion), "Fetching latest Fabric version"))[0] const file: HorizrFile = { formatVersion: CURRENT_HORIZR_FILE_FORMAT_VERSION, meta: { name: answers.name, version: "1.0.0", description: answers.description === "" ? undefined : answers.description, authors: (answers.authors as string).split(", ").map(a => a.trim()), license: answers.license }, versions: { minecraft: answers.minecraftVersion, fabric: fabricVersion } } await fs.writeJson(horizrFilePath.toString(), file, { spaces: 2 }) await fs.writeFile(directoryPath.resolve(".gitignore").toString(), "/generated/") const relativePath = Path.create(process.cwd()).relative(directoryPath).toString() if (relativePath === "") output.println(kleur.green(`Successfully initialized pack.`)) else output.println(kleur.green(`Successfully initialized pack in ${kleur.yellow(relativePath)}.`)) }) program.command("info", { isDefault: true }) .description("Print information about the pack.") .action(async () => { const pack = await usePack() const disabledModsCount = pack.mods.filter(mod => !mod.modFile.enabled).length const { description } = pack.horizrFile.meta output.println(dedent` ${kleur.underline(pack.horizrFile.meta.name)} ${kleur.dim(`(${pack.horizrFile.meta.version})`)} ${description === undefined ? "" : wrapAnsi(description, process.stdout.columns) + "\n"}\ Authors: ${kleur.yellow(pack.horizrFile.meta.authors.join(", "))} License: ${kleur.yellow(pack.horizrFile.meta.license.toUpperCase())} Mods: ${kleur.yellow(pack.mods.length.toString())}${disabledModsCount === 0 ? "" : ` (${disabledModsCount} disabled)`} Minecraft version: ${kleur.yellow(pack.horizrFile.versions.minecraft)} `) }) program.command("remove ") .description("Remove the mod from the pack.") .action(async code => { const pack = await usePack() const mod = pack.findModByCodeOrFail(code) await removeModFile(pack.paths.root, mod.id) output.println(`${mod.modFile.name} ${kleur.green("was removed from the pack.")}`) }) program.command("update [code]") .description("Check for updates of all mods or update a specific mod") .option("-y, --yes", "Skip confirmations") .option("-b, --allow-beta", "Allow beta versions") .option("-a, --allow-alpha", "Allow alpha and beta versions") .action(async (code, options) => { const pack = await usePack() const allowedReleaseChannels = releaseChannelOrder.slice(releaseChannelOrder.indexOf(options.allowAlpha ? "alpha" : options.allowBeta ? "beta" : "release")) if (code === undefined) { const updates = await pack.checkForUpdates(allowedReleaseChannels) if (updates.length === 0) output.println(kleur.green("Everything up-to-date.")) else { output.println(dedent` ${kleur.underline("Available updates")} ${updates.map(update => `- ${kleur.gray(update.mod.id)} ${update.mod.modFile.name}: ${kleur.red(update.activeVersion)} ${figures.arrowRight} ${kleur.green(update.availableVersion)}`).join("\n")} `) } } else { const mod = pack.findModByCodeOrFail(code) const update = await output.withLoading(mod.checkForUpdate(allowedReleaseChannels), "Checking for an update") if (update === null) { output.println(kleur.green("No update available.")) } else { if (update.changelog === null) { output.println(`No changelog available for ${kleur.bold(update.availableVersion)}.`) } else { output.println(`${kleur.underline("Changelog")} for ${kleur.bold().yellow(update.availableVersion)}\n`) output.printlnWrapping(update.changelog) } output.println("") const confirmed = options.yes || (await enquirer.prompt({ type: "confirm", name: "confirmed", message: "Apply the update?" }) as any).confirmed if (confirmed) { await output.withLoading(update.apply(), "Updating") output.println(kleur.green(`Successfully updated ${kleur.yellow(update.mod.modFile.name)} to ${kleur.yellow(update.availableVersion)}.`)) } } } }) loudRejection(stack => { output.failAndExit(stack) }) await program .addCommand(packwizCommand) .addCommand(modrinthCommand) .addHelpText("after", "\n" + dedent` ${kleur.blue("code")} can be one of the following: - The name of a file in the ${kleur.yellow("mods")} directory, optionally without the ${kleur.yellow(".json")} extension - The ID of a Modrinth Project, prefixed with ${kleur.yellow("mr:")} - The ID of a Modrinth Version, prefixed with ${kleur.yellow("mrv:")} `) .parseAsync(process.argv)