Implement modrinth open and activate
This commit is contained in:
parent
585fd43708
commit
5a1c6f47e4
10 changed files with 106 additions and 529 deletions
77
README.md
77
README.md
|
@ -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 it’s not good either.
|
||||
I developed this tool primarily for my own packs, that’s why its missing some features I didn’t 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.
|
||||
|
|
|
@ -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
29
pnpm-lock.yaml
generated
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
`)
|
||||
|
|
24
src/commands/modrinth/open.ts
Normal file
24
src/commands/modrinth/open.ts
Normal 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.`)
|
||||
})
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 }
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
Add table
Reference in a new issue