cli/src/commands/packwiz.ts

201 lines
7.1 KiB
TypeScript

import { Command } from "commander"
import { Mod, usePack } from "../pack.js"
import fs from "fs-extra"
import dedent from "dedent"
import kleur from "kleur"
import { getLANAddress, getSha512HexHash, httpServeDirectory, optionParsePositiveInteger } from "../utils.js"
import { output } from "../output.js"
import { Visitor, walk } from "@root/walk"
import { Path } from "../path.js"
const packwizCommand = new Command("packwiz")
.alias("pw")
packwizCommand.command("import")
.description("Import a packwiz pack.")
.action(async () => {
output.failAndExit("Not implemented.")
// TODO: Import packwiz pack
})
packwizCommand.command("serve")
.description("Start an HTTP server in the packwiz directory.")
.option("-p, --port <port>", "The port of the HTTP server.", optionParsePositiveInteger, 8000)
.option("-e, --expose", "Expose the HTTP server on all interfaces.")
.action(async options => {
const pack = await usePack()
const directoryPath = pack.paths.generated.resolve("packwiz")
if (!(await fs.pathExists(directoryPath.toString())))
output.failAndExit(`The ${kleur.yellow("packwiz")} directory does not exist. Generate it by running ${kleur.yellow("horizr packwiz export")}.`)
await serveExportOutput(directoryPath, options.port, options.expose)
})
// packwizCommand.command("dev")
// .description("serve + export with hot-reloading.")
// .option("-s, --server", "Use server overrides instead of client overrides.")
// .option("-p, --port <port>", "The port of the HTTP server.", optionParsePositiveInteger, 8000)
// .option("-e, --expose", "Expose the HTTP server on all interfaces.")
// .action(async options => {
//
// })
packwizCommand.command("export")
.description("Generate a packwiz pack in the packwiz directory.")
.option("-s, --server", "Use server overrides instead of client overrides.")
.action(async options => {
await runExport(options.server)
})
async function runExport(forServer: boolean) {
const pack = await usePack()
const loader = output.startLoading("Generating")
const outputDirectoryPath = pack.paths.generated.resolve("packwiz")
await fs.remove(outputDirectoryPath.toString())
await fs.mkdirp(outputDirectoryPath.resolve("mods").toString())
const indexedFiles: IndexedFile[] = []
await indexMods(indexedFiles, outputDirectoryPath)
loader.setText(`Copying and hashing ${forServer ? "server" : "client"} overrides`)
await copyOverrides(indexedFiles, outputDirectoryPath, forServer)
loader.setText(`Writing ${kleur.yellow("index.toml")}`)
await writeIndexAndPackManifest(indexedFiles, outputDirectoryPath)
loader.stop()
output.println(kleur.green("Generated packwiz pack"))
return {
indexedFiles
}
}
interface IndexedFile {
path: string
sha512HashHex: string
isMeta: boolean
}
async function writeAndIndexModMetaFile(indexedFiles: IndexedFile[], outputDirectoryPath: Path, mod: Mod) {
const content = dedent`
name = ${JSON.stringify(mod.modFile.name)}
filename = ${JSON.stringify(mod.modFile.file.name)}
side = "${mod.modFile.side.replace("client-server", "both")}"
[download]
hash-format = "sha512"
hash = ${JSON.stringify(mod.modFile.file.hashes.sha512)}
url = ${JSON.stringify(mod.modFile.file.downloadUrl)}
`
const path = outputDirectoryPath.resolve(`mods/${mod.id}.toml`)
await fs.writeFile(path.toString(), content)
indexedFiles.push({
path: `mods/${mod.id}.toml`,
isMeta: true,
sha512HashHex: await getSha512HexHash(content)
})
}
async function indexMods(indexedFiles: IndexedFile[], outputDirectoryPath: Path, warn: boolean = true) {
const pack = await usePack()
for (const mod of pack.mods) {
if (warn && !mod.modFile.enabled) output.warn(`${kleur.yellow(mod.modFile.name)} is disabled and will not be included.`)
await output.withLoading(
writeAndIndexModMetaFile(indexedFiles, outputDirectoryPath, mod),
`Generating ${kleur.yellow(mod.id + ".toml")} (${indexedFiles.length + 1}/${pack.mods.length})`
)
}
}
async function copyOverrides(indexedFiles: IndexedFile[], outputDirectoryPath:Path, forServer: boolean) {
const pack = await usePack()
const createVisitor = (overridesDirectoryPath: Path): Visitor => async (error, path, dirent) => {
const relativePath = overridesDirectoryPath.relative(path)
if (error) output.warn(`${kleur.yellow(relativePath.toString())}: ${error.message}`)
else {
if (dirent.name.startsWith(".")) return false
if (dirent.isFile()) {
const outputPath = outputDirectoryPath.resolve(relativePath)
await fs.mkdirp(outputPath.getParent().toString())
await fs.copy(path, outputPath.toString())
indexedFiles.push({
path: relativePath.toString(),
isMeta: false,
sha512HashHex: await getSha512HexHash(await fs.readFile(overridesDirectoryPath.resolve(path).toString()))
})
}
}
}
const specificOverridesDirectoryPath = pack.paths.overrides[forServer ? "server" : "client"]
const universalOverridesDirectoryPath = pack.paths.overrides["client-server"]
if (await fs.pathExists(specificOverridesDirectoryPath.toString())) await walk(specificOverridesDirectoryPath.toString(), createVisitor(specificOverridesDirectoryPath))
if (await fs.pathExists(universalOverridesDirectoryPath.toString())) await walk(universalOverridesDirectoryPath.toString(), createVisitor(universalOverridesDirectoryPath))
}
async function writeIndexAndPackManifest(indexedFiles: IndexedFile[], outputDirectoryPath: Path) {
const pack = await usePack()
const index = dedent`
hash-format = "sha512"
${indexedFiles.map(file => dedent`
[[files]]
file = ${JSON.stringify(file.path)}
hash = "${file.sha512HashHex}"
metafile = ${file.isMeta}
`).join("\n\n")}
`
await fs.writeFile(outputDirectoryPath.resolve("index.toml").toString(), index)
const indexHash = await getSha512HexHash(index)
await fs.writeFile(outputDirectoryPath.resolve("pack.toml").toString(), dedent`
name = ${JSON.stringify(pack.horizrFile.meta.name)}
authors = ${JSON.stringify(pack.horizrFile.meta.authors.join(", "))}\
${pack.horizrFile.meta.description === undefined ? "" : "\n" + `description = ${JSON.stringify(pack.horizrFile.meta.description)}`}
pack-format = "packwiz:1.0.0"
[versions]
minecraft = ${JSON.stringify(pack.horizrFile.versions.minecraft)}
fabric = ${JSON.stringify(pack.horizrFile.versions.fabric)}
[index]
file = "index.toml"
hash-format = "sha512"
hash = "${indexHash}"
`)
}
async function serveExportOutput(path: Path, port: number, expose: boolean) {
const lanAddress = await getLANAddress()
const localAddress = `http://localhost:${port}/pack.toml`
await new Promise<void>(resolve => {
httpServeDirectory(path, port, expose, () => {
if (expose) {
output.println(dedent`
${kleur.green("Serving at")}
Local: ${kleur.yellow(localAddress)}
Network: ${kleur.yellow(`http://${lanAddress}:${port}/pack.toml`)}
`)
} else output.println(`${kleur.green("Serving at")} ${kleur.yellow(localAddress)}`)
resolve()
})
})
}
export { packwizCommand}