Implement modrinth open and activate

This commit is contained in:
Moritz Ruth 2022-08-18 13:28:32 +02:00
parent 585fd43708
commit 5a1c6f47e4
10 changed files with 106 additions and 529 deletions

View file

@ -6,11 +6,7 @@
> A CLI tool for creating and maintaining Minecraft modpacks using the Fabric loader.
🎉 Features:
- Access [Modrinth](https://modrinth.com/)
- Search
- Add mods by ID or slug
- View available versions
- View dependencies
- Add mods from [Modrinth](https://modrinth.com/)
- Check for updates and view changelogs before applying them
- Export the pack to the [Modrinth format (`.mrpack`)](https://docs.modrinth.com/docs/modpacks/format_definition/)
- Export the pack to the [`packwiz`](https://packwiz.infra.link/) format
@ -31,76 +27,9 @@ Run any command with the `-h` flag to see the available options.
A new pack can be initialized using `horizr init <path>`.
## Examples
- Activate the latest (compatible) version of [Charm](https://modrinth.com/mod/charm)
```sh
$ horizr modrinth mod activate charm
# or short:
$ horizr mr mod a charm
```
- Activate `v4.1.1` of [Charm](https://modrinth.com/mod/charm)
```sh
$ horizr modrinth mod versions charm
# `BT9G1Jjs` is the version code you are looking for.
# This output will be colored in your console.
BT9G1Jjs 4.2.0+1.18.2 (↓ 137)
featured
Name: [1.18.2] 4.2.0
Channel: release
Minecraft versions: 1.18.2
Publication: last week
https://modrinth.com/mod/pOQTcQmj/version/BT9G1Jjs
# … more versions omitted for brevity
$ horizr modrinth version activate BT9G1Jjs
Charm (4.2.0+1.18.2) was successfully activated.
Dependencies
◉ Fabric API (P7dR8mSH): any version
```
- Check for updates
```sh
$ horizr update
# Because Sodium's version string is not a valid SemVer,
# the publication date will instead be used for comparison.
Sodium has no valid semantic version: mc1.18.2-0.4.1. The
publication date will instead be used.
Available updates
- charm Charm: 4.1.0+1.18.2 → 4.2.0+1.18.2
```
- Apply an update
```sh
$ horizr update charm
Changelog for 4.2.0+1.18.2
* Added ebony wood.
* Fixed issue with Totems not always spawning or being
carried away by mobs.
# … omitted for brevity
Apply the update? [Y/n] y
Successfully updated Charm to 4.2.0+1.18.2.
```
## Contributing
I developed this tool primarily for my own packs, soooo… the code quality is not bad, but its not good either.
I developed this tool primarily for my own packs, thats why its missing some features I didnt absolutely need.
If you want a feature added, feel free to [create an issue](https://github.com/horizr/cli/issues/new).
Nevertheless, if you want a feature added, feel free to [create an issue](https://github.com/horizr/cli/issues/new).
A pull request would be even better.

View file

@ -8,7 +8,8 @@
"repository": "https://github.com/horizr/cli",
"scripts": {
"start": "tsx src/main.ts",
"build": "del dist && tsc"
"build": "del dist && tsc",
"check": "tsc --noEmit"
},
"bin": {
"horizr": "bin/horizr.js"
@ -35,6 +36,7 @@
"lodash-es": "^4.17.21",
"loud-rejection": "^2.2.0",
"nanoid": "^4.0.0",
"open": "^8.4.0",
"ora": "^6.1.2",
"p-event": "^5.0.1",
"p-limit": "^4.0.0",

29
pnpm-lock.yaml generated
View file

@ -27,6 +27,7 @@ specifiers:
lodash-es: ^4.17.21
loud-rejection: ^2.2.0
nanoid: ^4.0.0
open: ^8.4.0
ora: ^6.1.2
p-event: ^5.0.1
p-limit: ^4.0.0
@ -59,6 +60,7 @@ dependencies:
lodash-es: 4.17.21
loud-rejection: 2.2.0
nanoid: 4.0.0
open: 8.4.0
ora: 6.1.2
p-event: 5.0.1
p-limit: 4.0.0
@ -510,6 +512,11 @@ packages:
engines: {node: '>=10'}
dev: false
/define-lazy-prop/2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
dev: false
/del-cli/5.0.0:
resolution: {integrity: sha512-rENFhUaYcjoMODwFhhlON+ogN7DoG+4+GFN+bsA1XeDt4w2OKQnQadFP1thHSAlK9FAtl88qgP66wOV+eFZZiQ==}
engines: {node: '>=14.16'}
@ -1023,6 +1030,12 @@ packages:
has: 1.0.3
dev: true
/is-docker/2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
hasBin: true
dev: false
/is-extglob/2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@ -1067,6 +1080,13 @@ packages:
engines: {node: '>=12'}
dev: false
/is-wsl/2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
dependencies:
is-docker: 2.2.1
dev: false
/js-tokens/4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: true
@ -1303,6 +1323,15 @@ packages:
mimic-fn: 2.1.0
dev: false
/open/8.4.0:
resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==}
engines: {node: '>=12'}
dependencies:
define-lazy-prop: 2.0.0
is-docker: 2.2.1
is-wsl: 2.2.0
dev: false
/ora/6.1.2:
resolution: {integrity: sha512-EJQ3NiP5Xo94wJXIzAyOtSb0QEIAUu7m8t6UZ9krbz0vAJqr92JpcK/lEXg91q6B9pEGqrykkd2EQplnifDSBw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}

View file

@ -4,7 +4,7 @@ import {
findMetaFileForModrinthMod,
getMetaFileContentVersionForModrinth,
getSideOfModrinthMod,
isModrinthVersionCompatible,
isModrinthVersionCompatible, resolveFullRelation,
resolveModrinthCode,
sortModrinthVersionsByPreference
} from "../../modrinth/index.js"
@ -15,6 +15,7 @@ import kleur from "kleur"
import { META_FILE_EXTENSION, metaFileContentSchema, writeJsonFile } from "../../files.js"
import fs from "fs-extra"
import enquirer from "enquirer"
import { orEmptyString } from "../../utils/strings.js"
export const activateCommand = new Command("activate")
.argument("<code>")
@ -27,7 +28,7 @@ export const activateCommand = new Command("activate")
const modrinthMod = resolvedCode.modrinthMod
let modrinthVersion = resolvedCode.modrinthVersion
const existingMetaFile = findMetaFileForModrinthMod(pack.metaFiles, modrinthMod)
const existingMetaFile = findMetaFileForModrinthMod(pack.metaFiles, modrinthMod.id)
if (existingMetaFile !== null) {
output.println(`The mod is already active: ${kleur.yellow(existingMetaFile.relativePath.toString())} ${kleur.blue(existingMetaFile.content.version.name)}`)
@ -77,6 +78,22 @@ export const activateCommand = new Command("activate")
})
await pack.registerCreatedSourceFile(relativePath)
output.println(kleur.green(`Successfully wrote ${kleur.yellow(relativePath.toString())}`))
const loader = output.startLoading("Checking dependencies")
for (const relation of modrinthVersion.relations) {
if (relation.type === "hard_dependency") {
const { modrinthMod, modrinthVersion } = await resolveFullRelation(relation)
const metaFile = await findMetaFileForModrinthMod(pack.metaFiles, modrinthMod.id)
if (metaFile === null) {
const versionString = orEmptyString(modrinthVersion, v => ` ${kleur.blue(v.versionString)}`)
const idString = kleur.gray(modrinthMod.slug + orEmptyString(modrinthVersion, v => `@${v.versionString}`))
output.warn(`Unmet dependency: ${kleur.yellow(modrinthMod.title)}${versionString} ${idString}`)
}
}
}
loader.stop()
})

View file

@ -2,15 +2,17 @@ import { Command } from "commander"
import { activateCommand } from "./activate.js"
import dedent from "dedent"
import kleur from "kleur"
import { openCommand } from "./open.js"
export const modrinthCommand = new Command("modrinth")
.alias("mr")
.addCommand(activateCommand)
.addHelpText("after", dedent`
${kleur.yellow("<code>")} may be one of the following:
.addCommand(openCommand)
.addHelpText("afterAll", dedent`
\n${kleur.yellow("<code>")} may be one of the following:
- URL or slug of a Modrinth mod (${kleur.yellow("https://modrinth.com/mod/sodium")} or ${kleur.yellow("sodium")})
- URL of a Modrinth mod version (${kleur.yellow("https://modrinth.com/mod/sodium/version/mc1.19-0.4.2")})
- slug of a Modrinth mod and a version with a ${kleur.yellow("@")} in between (${kleur.yellow("sodium:mc1.19-0.4.2")})
- slug of a Modrinth mod and a version with a ${kleur.yellow("@")} in between (${kleur.yellow("sodium@mc1.19-0.4.2")})
- Modrinth project ID (${kleur.yellow("AANobbMI")} for Sodium)
- Modrinth version ID, prefixed with ${kleur.yellow("@")} (${kleur.yellow("@Yp8wLY1P")} for Sodium mc1.19-0.4.2)
`)

View file

@ -0,0 +1,24 @@
import { Command } from "commander"
import { usePack } from "../../pack.js"
import { output } from "../../utils/output.js"
import kleur from "kleur"
import open from "open"
export const openCommand = new Command("open")
.argument("<path>")
.action(async pathString => {
const pack = await usePack()
const metaFile = pack.getMetaFileFromInput(pathString)
if (metaFile.content.source?.type === "modrinth") {
const { modId } = metaFile.content.source
const url = `https://modrinth.com/mod/${encodeURIComponent(modId)}`
try {
await open(url, { wait: false })
output.printlnWrapping(kleur.green(`Opened ${kleur.yellow(url)} in your default browser.`))
} catch (e: unknown) {
output.fail(`Could not open ${kleur.yellow(url)} in a browser.`)
}
} else output.failAndExit(`${kleur.yellow(metaFile.relativePath.toString())} is not a Modrinth mod.`)
})

View file

@ -1,5 +1,5 @@
import { IterableElement } from "type-fest"
import { modrinthApi, ModrinthMod, ModrinthVersion, ModrinthVersionFile } from "./api.js"
import { modrinthApi, ModrinthMod, ModrinthVersion, ModrinthVersionFile, ModrinthVersionRelation } from "./api.js"
import { sortBy } from "lodash-es"
import { MetaFile, Pack, releaseChannelOrder } from "../pack.js"
import { MetaFileContentVersion } from "../files.js"
@ -106,5 +106,23 @@ export async function resolveModrinthCode(code: string): Promise<{ modrinthMod:
return output.failAndExit(`Invalid ${kleur.yellow("<code>")}: ${kleur.yellow(code)}`)
}
export const findMetaFileForModrinthMod = (metaFiles: MetaFile[], modrinthMod: ModrinthMod) =>
metaFiles.find(metaFile => metaFile.content.source?.type === "modrinth" && metaFile.content.source.modId === modrinthMod.id) ?? null
export const findMetaFileForModrinthMod = (metaFiles: MetaFile[], modrinthModId: string) =>
metaFiles.find(metaFile => metaFile.content.source?.type === "modrinth" && metaFile.content.source.modId === modrinthModId) ?? null
export async function resolveFullRelation(relation: ModrinthVersionRelation) {
if (relation.projectId === null) {
const modrinthVersion = (await modrinthApi.getVersion(relation.versionId!))!
return {
modrinthVersion,
modrinthMod: (await modrinthApi.getMod(modrinthVersion.projectId))!
}
} else {
const modrinthMod = (await modrinthApi.getMod(relation.projectId))!
return {
modrinthMod,
modrinthVersion: null
}
}
}

View file

@ -140,14 +140,13 @@ export const output = {
}
},
println(text: string) {
this.print(text + "\n")
this.print(text + "\n\n")
},
printlnWrapping(text: string) {
this.println(wrapAnsi(text, process.stdout.columns))
},
warn(text: string) {
this.printlnWrapping(`${kleur.yellow(figures.pointer)} ${text}`)
this.println("")
},
fail(text: string) {
last(loadersStack)?.fail()

View file

@ -1,396 +0,0 @@
import { Command } from "commander"
import { take } from "lodash-es"
import { usePack } from "../../pack.js"
import kleur from "kleur"
import { optionParsePositiveInteger, truncateWithEllipsis, zipDirectory } from "../../utils.js"
import { default as wrapAnsi } from "wrap-ansi"
import figures from "figures"
import {
modrinthApi,
ModrinthMod,
ModrinthVersion,
ModrinthVersionRelation,
} from "../../modrinth/api.js"
import dedent from "dedent"
import ago from "s-ago"
import semver from "semver"
import { output } from "../../../src/utils/output.js"
import fs from "fs-extra"
import { addModrinthMod, findModForModrinthMod, getModFileDataForModrinthVersion, isModrinthVersionCompatible, sortModrinthVersionsByPreference } from "../../modrinth/utils.js"
import { walk } from "@root/walk"
const modrinthCommand = new Command("modrinth")
.alias("mr")
modrinthCommand.command("search <query...>")
.description("Search for mods.")
.option("-l, --limit <number>", "Limit the number of results", optionParsePositiveInteger, 8)
.option("-s, --skip <number>", "Skip results", optionParsePositiveInteger, 0)
.action(async (query, options) => {
const pack = await usePack()
const loader = output.startLoading(`Searching for ${kleur.yellow(query)}`)
const { results } = await modrinthApi.searchMods(pack.horizrFile.versions.minecraft, query, options)
loader.stop()
output.println(
results.map(result =>
`${kleur.blue(result.id)} ${kleur.bold(truncateWithEllipsis(result.title, 30))} ${kleur.gray(`(↓ ${result.downloadsCount})`)}\n` +
wrapAnsi(result.description, process.stdout.columns)
)
.join("\n\n")
)
})
const colorBySideCompatibility: Record<ModrinthMod["clientSide"], kleur.Color> = {
optional: kleur.blue,
required: kleur.green,
unsupported: kleur.red
}
const modrinthModCommand = modrinthCommand.command("mod")
modrinthModCommand.command("info <id>")
.description("Show information about the mod.")
.action(async id => {
const loader = output.startLoading("Fetching mod information")
const modrinthMod = await modrinthApi.getMod(id)
if (modrinthMod === null) return loader.failAndExit("not found")
loader.stop()
const existingMod = await findModForModrinthMod(modrinthMod)
output.println(dedent`
${kleur.bold(modrinthMod.title)} ${kleur.gray(`(↓ ${modrinthMod.downloadsCount})`)}
${wrapAnsi(modrinthMod.description, process.stdout.columns)}
Client Server
${colorBySideCompatibility[modrinthMod.clientSide](modrinthMod.clientSide.padEnd(12, " "))} ${colorBySideCompatibility[modrinthMod.serverSide](modrinthMod.serverSide)}
License: ${kleur.yellow(modrinthMod.licenseCode.toUpperCase())}
Last update: ${kleur.yellow(ago(modrinthMod.updateDate))}\
${existingMod === null ? "" : kleur.green("\n\nThis mod is in the pack.")}
https://modrinth.com/mod/${modrinthMod.slug}
`)
})
modrinthModCommand.command("versions <id>")
.description("Show a list of compatible versions of the mod.")
.option("-l, --limit <number>", "Limit the number of versions displayed.", optionParsePositiveInteger, 3)
.action(async (id, options) => {
const pack = await usePack()
const loader = output.startLoading("Fetching mod information")
const modrinthMod = await modrinthApi.getMod(id)
if (modrinthMod === null) return loader.failAndExit("not found")
else loader.stop()
const existingMod = await findModForModrinthMod(modrinthMod)
const modrinthVersions = await output.withLoading(modrinthApi.listVersions(id, pack.horizrFile.versions.minecraft), "Fetching versions")
if (modrinthVersions.length === 0) {
const message =
`There are no versions compatible with the pack (Fabric ${kleur.yellow(pack.horizrFile.versions.fabric)}, Minecraft ${kleur.yellow(pack.horizrFile.versions.minecraft)}).`
output.println(kleur.red(message))
} else {
const versions = take(sortModrinthVersionsByPreference(modrinthVersions), options.limit)
.map(modrinthVersion => {
const state = existingMod !== null && existingMod.modFile.source.versionId === modrinthVersion.id
? kleur.bgGreen().black(" active ") + "\n\n"
: modrinthVersion.isFeatured
? kleur.green("featured") + "\n\n"
: ""
return dedent`
${kleur.blue(modrinthVersion.id)} ${kleur.bold(modrinthVersion.versionString)} ${kleur.gray(`(↓ ${modrinthVersion.downloadsCount})`)}
${state}\
${modrinthVersion.name !== modrinthVersion.versionString ? `Name: ${kleur.yellow(modrinthVersion.name)}\n` : ""}\
Channel: ${kleur.yellow(modrinthVersion.releaseChannel)}
Minecraft versions: ${kleur.yellow(modrinthVersion.supportedMinecraftVersions.join(", "))}
Publication: ${kleur.yellow(ago(modrinthVersion.publicationDate))}
https://modrinth.com/mod/${modrinthVersion.projectId}/version/${modrinthVersion.id}
`
})
.join("\n\n")
output.println(versions)
}
})
modrinthModCommand.command("activate <id>")
.description("Activate the recommended version of the mod.")
.alias("a")
.option("-f, --force", "Replace a different version already active.")
.action(async (id, options) => {
const pack = await usePack()
const loader = output.startLoading("Fetching mod information")
const modrinthMod = await modrinthApi.getMod(id)
if (modrinthMod === null) return loader.failAndExit("not found")
else loader.stop()
const modrinthVersions = await output.withLoading(modrinthApi.listVersions(id, pack.horizrFile.versions.minecraft), "Fetching versions")
if (modrinthVersions.length === 0) return output.failAndExit("There is no compatible version of this mod.")
const sortedModrinthVersions = sortModrinthVersionsByPreference(modrinthVersions)
const modrinthVersion = sortedModrinthVersions[0]
await handleActivate(modrinthMod, modrinthVersion, options.force)
})
const colorByRelationType: Record<ModrinthVersionRelation["type"], kleur.Color> = {
"embedded_dependency": kleur.green,
"soft_dependency": kleur.magenta,
"hard_dependency": kleur.yellow,
"incompatible": kleur.red
}
const nullVersionStringByRelationType: Record<ModrinthVersionRelation["type"], string> = {
"embedded_dependency": "unknown version",
"soft_dependency": "any version",
"hard_dependency": "any version",
"incompatible": "all versions"
}
const versionStateStrings = {
"active": kleur.bgGreen().black(" active "),
"compatible": kleur.blue("compatible"),
"incompatible": kleur.red("incompatible"),
"newer_version": `${kleur.bgYellow().black(" older version active ")} ${figures.arrowRight} EXISTING_VERSION`,
"older_version": `${kleur.bgYellow().black(" newer version active ")} ${figures.arrowRight} EXISTING_VERSION`,
"different_version": `${kleur.bgYellow().black(" different version active ")} ${figures.arrowRight} EXISTING_VERSION`
}
async function getRelationsListLines(relations: ModrinthVersionRelation[]) {
return await Promise.all(relations.map(async relation => {
const color = colorByRelationType[relation.type]
const relatedVersion = relation.versionId === null ? null : (await modrinthApi.getVersion(relation.versionId))
const versionString = relatedVersion === null ? nullVersionStringByRelationType[relation.type] : relatedVersion.versionString
const relatedMod = (await modrinthApi.getMod(relation.projectId === null ? relatedVersion!.projectId : relation.projectId))!
return `${color(figures.circleFilled)} ${relatedMod.title}${relation.projectId ? ` (${kleur.blue(relation.projectId)})` : ""}: ` +
`${versionString}${relation.versionId ? ` (${kleur.blue(relation.versionId)})` + " " : ""}`
}))
}
const modrinthVersionCommand = modrinthCommand.command("version")
modrinthVersionCommand.command("info <id>")
.description("Show information about the version.")
.option("-c, --changelog", "Show the changelog.")
.action(async (id, options) => {
const pack = await usePack()
const loader = output.startLoading("Fetching version information")
const modrinthVersion = await modrinthApi.getVersion(id)
if (modrinthVersion === null) return loader.failAndExit("not found")
loader.setText("Fetching mod information")
const modrinthMod = (await modrinthApi.getMod(modrinthVersion.projectId))!
const existingMod = await findModForModrinthMod(modrinthMod)
let state: keyof typeof versionStateStrings
if (existingMod === null) state = isModrinthVersionCompatible(modrinthVersion, pack) ? "compatible" : "incompatible"
else {
if (existingMod.modFile.source.versionId === modrinthVersion.id) state = "active"
else {
const existingSemver = semver.parse(existingMod.modFile.file.version)
const newSemver = semver.parse(modrinthVersion.versionString)
if (existingSemver === null || newSemver === null) state = "different_version"
else {
const comparison = newSemver.compare(existingSemver)
if (comparison === 1) state = "newer_version"
else if (comparison === -1) state = "older_version"
else state = "active" // this should not happen: the versionString is the same but the versionId is different
}
}
}
loader.setText("Resolving relations")
const relationsList = modrinthVersion.relations.length !== 0 ? (await getRelationsListLines(modrinthVersion.relations)).join("\n") : kleur.gray("none")
const relationsColorKey = `${colorByRelationType.hard_dependency("hard dependency")}, ${colorByRelationType.soft_dependency("soft dependency")}, ` +
`${colorByRelationType.embedded_dependency("embedded")}, ${colorByRelationType.incompatible("incompatible")}`
loader.stop()
output.println(dedent`
${kleur.underline(modrinthMod.title)} ${kleur.yellow(`${modrinthVersion.versionString} (${modrinthVersion.releaseChannel})`)}
${versionStateStrings[state].replace("EXISTING_VERSION", existingMod?.modFile?.file.version ?? "ERROR")}
Version name: ${kleur.yellow(modrinthVersion.name)} ${kleur.gray(ago(modrinthVersion.publicationDate))}
Minecraft versions: ${modrinthVersion.supportedMinecraftVersions.map(version => version === pack.horizrFile.versions.minecraft ? kleur.green(version) : kleur.red(version)).join(", ")}
Loaders: ${modrinthVersion.supportedLoaders.map(loader => loader === "fabric" ? kleur.green(loader) : kleur.red(loader)).join(", ")}
Related mods: ${relationsColorKey}
${relationsList}
https://modrinth.com/mod/${modrinthMod.slug}/version/${modrinthVersion.versionString}
`)
if (options.changelog) {
output.println("")
output.println(kleur.underline("Changelog"))
if (modrinthVersion.changelog === null) output.println(kleur.gray("not available"))
else output.printlnWrapping(modrinthVersion.changelog)
}
})
modrinthVersionCommand.command("activate <id>")
.description("Activate the mod version.")
.alias("a")
.option("-f, --force", "Replace a different version already active.")
.action(async (id, options) => {
const pack = await usePack()
const loader = output.startLoading("Fetching version information")
const modrinthVersion = await modrinthApi.getVersion(id)
if (modrinthVersion === null) return loader.failAndExit("not found")
loader.setText("Fetching mod information")
const modrinthMod = (await modrinthApi.getMod(modrinthVersion.projectId))!
loader.stop()
if (!isModrinthVersionCompatible(modrinthVersion, pack)) return output.failAndExit("This version is not compatible with the pack.")
await handleActivate(modrinthMod, modrinthVersion, options.force)
})
modrinthCommand.command("export")
.description("Export a Modrinth pack.")
.option("-s, --no-generate", "Skip regenerating the output directory.")
.option("-z, --no-zip", "Skip creating a zipped .mrpack file.")
.option("-c, --clear", "Remove the output directory afterwards.")
.action(async options => {
const pack = await usePack()
const outputDirectory = pack.paths.generated.resolve("modrinth-pack")
if (options.generate) {
const loader = output.startLoading("Generating")
await pack.validateOverridesDirectories()
await fs.remove(outputDirectory.toString())
await fs.mkdirp(outputDirectory.toString())
await fs.writeJson(outputDirectory.resolve("modrinth.index.json").toString(), {
formatVersion: 1,
game: "minecraft",
versionId: pack.horizrFile.meta.version,
name: pack.horizrFile.meta.name,
summary: pack.horizrFile.meta.description,
dependencies: {
minecraft: pack.horizrFile.versions.minecraft,
"fabric-loader": pack.horizrFile.versions.fabric
},
files: pack.mods.map(mod => ({
path: `mods/${mod.modFile.file.name}`,
hashes: {
sha1: mod.modFile.file.hashes.sha1,
sha512: mod.modFile.file.hashes.sha512
},
env: {
client: mod.modFile.side === "client" || mod.modFile.side === "client-server" ? "required" : "unsupported",
server: mod.modFile.side === "server" || mod.modFile.side === "client-server" ? "required" : "unsupported"
},
downloads: [
mod.modFile.file.downloadUrl
],
fileSize: mod.modFile.file.size
}))
}, { spaces: 2 })
if (await fs.pathExists(pack.paths.overrides["client-server"].toString())) await output.withLoading(
fs.copy(pack.paths.overrides["client-server"].toString(), outputDirectory.resolve("overrides").toString(), { recursive: true }),
"Copying client-server overrides"
)
if (await fs.pathExists(pack.paths.overrides["client"].toString())) {
await output.withLoading(
fs.copy(pack.paths.overrides["client"].toString(), outputDirectory.resolve("client-overrides").toString(), { recursive: true }),
"Copying client overrides"
)
// Workaround for https://github.com/PolyMC/PolyMC/issues/1060
await walk(pack.paths.overrides["client"].toString(), async (error, path, dirent) => {
if (error) return
if (dirent.isDirectory()) {
const relativePath = pack.paths.overrides["client"].relative(path)
await fs.mkdirp(outputDirectory.resolve("overrides", relativePath).toString())
}
return true
})
}
if (await fs.pathExists(pack.paths.overrides["server"].toString())) await output.withLoading(
fs.copy(pack.paths.overrides["server"].toString(), outputDirectory.resolve("server-overrides").toString(), { recursive: true }),
"Copying server overrides"
)
output.println(kleur.green(`Generated Modrinth pack`))
loader.stop()
}
if (options.zip) {
if (!(await fs.pathExists(outputDirectory.toString())))
output.failAndExit(`The ${kleur.yellow("modrinth-pack")} directory does not exist.\nRun the command without ${kleur.yellow("--no-generate")} to create it.`)
await output.withLoading(zipDirectory(outputDirectory, pack.paths.generated.resolve("pack.mrpack")), `Creating ${kleur.yellow(".mrpack")} file`)
output.println(kleur.green(`Created ${kleur.yellow("pack.mrpack")}`))
}
if (options.clear) {
await fs.remove(outputDirectory.toString())
output.println(kleur.green(`Removed the ${kleur.yellow("modrinth-pack")} directory`))
}
})
async function handleActivate(modrinthMod: ModrinthMod, modrinthVersion: ModrinthVersion, force: boolean) {
const existingMod = await findModForModrinthMod(modrinthMod)
if (existingMod === null) {
await addModrinthMod(modrinthMod, modrinthVersion)
output.println(`${modrinthMod.title} (${modrinthVersion.versionString}) ${kleur.green("was successfully activated.")}\n`)
await handleDependencies(modrinthVersion.relations)
} else {
const oldVersion = existingMod.modFile.file.version
if (existingMod.modFile.source.versionId === modrinthVersion.id) {
output.println(kleur.green("This version is already installed."))
} else if (force) {
existingMod.modFile.file = getModFileDataForModrinthVersion(modrinthMod, modrinthVersion)
existingMod.modFile.source.versionId = modrinthVersion.id
await existingMod.saveModFile()
output.println(`${kleur.green("Successfully replaced version")} ${oldVersion} ${kleur.green("of")} ${modrinthMod.title} ${kleur.green("with")} ${modrinthVersion.versionString}${kleur.green(".")}`)
await handleDependencies(modrinthVersion.relations)
} else {
output.failAndExit(`There is already a different version of this mod installed.\nRun this command again with ${kleur.yellow("-f")} to change the version.`)
}
}
}
async function handleDependencies(relations: ModrinthVersionRelation[]) {
const loader = output.startLoading("Fetching dependency information")
const lines = await getRelationsListLines(relations.filter(relation => relation.type === "hard_dependency" || relation.type === "soft_dependency"))
if (lines.length !== 0) {
output.println(dedent`
\n${kleur.underline("Dependencies")} ${colorByRelationType.hard_dependency("hard")}, ${colorByRelationType.soft_dependency("soft")}
${lines.join("\n")}
`)
}
loader.stop()
}
export { modrinthCommand }

View file

@ -1,47 +0,0 @@
import { IterableElement } from "type-fest"
import { sortBy } from "lodash-es"
import { Mod, Pack, usePack } from "../pack.js"
import { ModFile, ModFileData, MetaFileModrinthSource } from "../files.js"
import { pathExists } from "fs-extra"
import { nanoid } from "nanoid/non-secure"
import { output } from "../../src/utils/output.js"
import kleur from "kleur"
import { ModrinthMod, ModrinthVersion, ModrinthVersionFile } from "./api.js"
import { releaseChannelOrder, Side } from "../shared.js"
export async function addModrinthMod(modrinthMod: ModrinthMod, modrinthVersion: ModrinthVersion, side?: Side) {
const pack = await usePack()
let id = modrinthMod.slug
if (await pathExists(pack.paths.mods.resolve(`${id}.json`).toString())) {
const oldId = id
id = `${id}-${nanoid(5)}`
output.warn(
`There is already a mod file named ${kleur.yellow(`${oldId}.json`)} specifying a non-Modrinth mod.\n` +
`The file for this mod will therefore be named ${kleur.yellow(`${id}.json`)}`
)
}
if (side === undefined) {
const isClientSupported = modrinthMod.clientSide !== "unsupported"
const isServerSupported = modrinthMod.serverSide !== "unsupported"
side = isClientSupported && isServerSupported ? "client-server" : isClientSupported ? "client" : "server"
}
await pack.addMod(id, {
name: modrinthMod.title,
enabled: true,
ignoreUpdates: false,
side,
file: getModFileDataForModrinthVersion(modrinthMod, modrinthVersion),
source: {
type: "modrinth",
modId: modrinthMod.id,
versionId: modrinthVersion.id
}
})
}