{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Generate Distribution",
"program": "${workspaceFolder}\\src\\index.ts",
"args": [
"g", "distro"
],
"preLaunchTask": "build",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"outputCapture": "std",
},
{
"type": "node",
"request": "launch",
"name": "Install Dev Distribution",
"program": "${workspaceFolder}\\src\\index.ts",
"args": [
"g", "distro", "distribution_dev", "--installLocal"
],
"preLaunchTask": "build",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"outputCapture": "std"
}
]
}
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "compile",
"type": "typescript",
"tsconfig": "tsconfig.json",
"problemMatcher": [
"$tsc"
],
"presentation": {
"reveal": "silent"
}
},
{
"label": "build",
"type": "npm",
"script": "build",
"group": "build",
"problemMatcher": [],
"presentation": {
"reveal": "silent"
}
}
]
}
import dotenv from 'dotenv'
import { writeFile } from 'fs/promises'
import { resolve as resolvePath } from 'path'
import { URL } from 'url'
import { inspect } from 'util'
import yargs from 'yargs/yargs'
import { Argv, CommandModule } from 'yargs'
import { hideBin } from 'yargs/helpers'
import { DistributionStructure } from './structure/spec_model/Distribution.struct.js'
import { ServerStructure } from './structure/spec_model/Server.struct.js'
import { VersionSegmentedRegistry } from './util/VersionSegmentedRegistry.js'
import { VersionUtil } from './util/VersionUtil.js'
import { MinecraftVersion } from './util/MinecraftVersion.js'
import { LoggerUtil } from './util/LoggerUtil.js'
import { generateSchemas } from './util/SchemaUtil.js'
import { CurseForgeParser } from './parser/CurseForgeParser.js'
dotenv.config()
const logger = LoggerUtil.getLogger('Index')
function getRoot(): string {
return resolvePath(process.env.ROOT!)
}
function getHeliosDataFolder(): string | null {
if(process.env.HELIOS_DATA_FOLDER) {
return resolvePath(process.env.HELIOS_DATA_FOLDER)
}
return null
}
function getBaseURL(): string {
let baseUrl = process.env.BASE_URL!
// Users must provide protocol in all other instances.
if (baseUrl.indexOf('//') === -1) {
if (baseUrl.toLowerCase().startsWith('localhost')) {
baseUrl = 'http://' + baseUrl
} else {
throw new TypeError('Please provide a URL protocol (ex. http:// or https://)')
}
}
return (new URL(baseUrl)).toString()
}
function installLocalOption(yargs: Argv): Argv {
return yargs.option('installLocal', {
describe: 'Install the generated distribution to your local Helios data folder.',
type: 'boolean',
demandOption: false,
global: false,
default: false
})
}
function discardOutputOption(yargs: Argv): Argv {
return yargs.option('discardOutput', {
describe: 'Delete cached output after it is no longer required. May be useful if disk space is limited.',
type: 'boolean',
demandOption: false,
global: false,
default: false
})
}
function invalidateCacheOption(yargs: Argv): Argv {
return yargs.option('invalidateCache', {
describe: 'Invalidate and delete existing caches as they are encountered. Requires fresh cache generation.',
type: 'boolean',
demandOption: false,
global: false,
default: false
})
}
// function rootOption(yargs: Argv) {
// return yargs.option('root', {
// describe: 'File structure root.',
// type: 'string',
// demandOption: true,
// global: true
// })
// .coerce({
// root: resolvePath
// })
// }
// function baseUrlOption(yargs: Argv) {
// return yargs.option('baseUrl', {
// describe: 'Base url of your file host.',
// type: 'string',
// demandOption: true,
// global: true
// })
// .coerce({
// baseUrl: (arg: string) => {
// // Users must provide protocol in all other instances.
// if (arg.indexOf('//') === -1) {
// if (arg.toLowerCase().startsWith('localhost')) {
// arg = 'http://' + arg
// } else {
// throw new TypeError('Please provide a URL protocol (ex. http:// or https://)')
// }
// }
// return (new URL(arg)).toString()
// }
// })
// }
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function namePositional(yargs: Argv) {
return yargs.option('name', {
describe: 'Distribution index file name.',
type: 'string',
default: 'distribution'
})
}
// -------------
// Init Commands
const initRootCommand: CommandModule = {
command: 'root',
describe: 'Generate an empty standard file structure.',
builder: (yargs) => {
// yargs = rootOption(yargs)
return yargs
},
handler: async (argv) => {
argv.root = getRoot()
logger.debug(`Root set to ${argv.root}`)
logger.debug('Invoked init root.')
try {
await generateSchemas(argv.root as string)
await new DistributionStructure(argv.root as string, '', false, false).init()
await new CurseForgeParser(argv.root as string, '').init()
logger.info(`Successfully created new root at ${argv.root}`)
} catch (error) {
logger.error(`Failed to init new root at ${argv.root}`, error)
}
}
}
const initCommand: CommandModule = {
command: 'init',
aliases: ['i'],
describe: 'Base init command.',
builder: (yargs) => {
return yargs
.command(initRootCommand)
},
handler: (argv) => {
argv._handled = true
}
}
// -----------------
// Generate Commands
const generateServerCommand: CommandModule = {
command: 'server ',
describe: 'Generate a new server configuration.',
builder: (yargs) => {
// yargs = rootOption(yargs)
return yargs
.positional('id', {
describe: 'Server id.',
type: 'string'
})
.positional('version', {
describe: 'Minecraft version.',
type: 'string'
})
.option('forge', {
describe: 'Forge version.',
type: 'string'
})
.option('fabric', {
describe: 'Fabric version.',
type: 'string'
})
.conflicts('forge', 'fabric')
},
handler: async (argv) => {
argv.root = getRoot()
logger.debug(`Root set to ${argv.root}`)
logger.debug(`Generating server ${argv.id} for Minecraft ${argv.version}.`,
`\n\t└ Forge version: ${argv.forge}`,
`\n\t└ Fabric version: ${argv.fabric}`
)
const minecraftVersion = new MinecraftVersion(argv.version as string)
if(argv.forge != null) {
if (VersionUtil.isPromotionVersion(argv.forge as string)) {
logger.debug(`Resolving ${argv.forge as string} Forge Version..`)
const version = await VersionUtil.getPromotedForgeVersion(minecraftVersion, argv.forge as string)
logger.debug(`Forge version set to ${version}`)
argv.forge = version
}
}
if(argv.fabric != null) {
if (VersionUtil.isPromotionVersion(argv.fabric as string)) {
logger.debug(`Resolving ${argv.fabric as string} Fabric Version..`)
const version = await VersionUtil.getPromotedFabricVersion(argv.fabric as string)
logger.debug(`Fabric version set to ${version}`)
argv.fabric = version
}
}
const serverStruct = new ServerStructure(argv.root as string, getBaseURL(), false, false)
await serverStruct.createServer(
argv.id as string,
minecraftVersion,
{
forgeVersion: argv.forge as string,
fabricVersion: argv.fabric as string
}
)
}
}
const generateServerCurseForgeCommand: CommandModule = {
command: 'server-curseforge ',
describe: 'Generate a new server configuration from a CurseForge modpack.',
builder: (yargs) => {
// yargs = rootOption(yargs)
return yargs
.positional('id', {
describe: 'Server id.',
type: 'string'
})
.positional('zipName', {
describe: 'The name of the modpack zip file.',
type: 'string'
})
},
handler: async (argv) => {
argv.root = getRoot()
logger.debug(`Root set to ${argv.root}`)
logger.debug(`Generating server ${argv.id} using CurseForge modpack ${argv.zipName} as a template.`)
const parser = new CurseForgeParser(argv.root as string, argv.zipName as string)
const modpackManifest = await parser.getModpackManifest()
const minecraftVersion = new MinecraftVersion(modpackManifest.minecraft.version)
// Extract forge version
// TODO Support fabric
const forgeModLoader = modpackManifest.minecraft.modLoaders.find(({ id }) => id.toLowerCase().startsWith('forge-'))
const forgeVersion = forgeModLoader != null ? forgeModLoader.id.substring('forge-'.length) : undefined
logger.debug(`Forge version set to ${forgeVersion}`)
const serverStruct = new ServerStructure(argv.root as string, getBaseURL(), false, false)
const createServerResult = await serverStruct.createServer(
argv.id as string,
minecraftVersion,
{
version: modpackManifest.version,
forgeVersion
}
)
if(createServerResult) {
await parser.enrichServer(createServerResult, modpackManifest)
}
}
}
const generateDistroCommand: CommandModule = {
command: 'distro [name]',
describe: 'Generate a distribution index from the root file structure.',
builder: (yargs) => {
yargs = installLocalOption(yargs)
yargs = discardOutputOption(yargs)
yargs = invalidateCacheOption(yargs)
yargs = namePositional(yargs)
return yargs
},
handler: async (argv) => {
argv.root = getRoot()
argv.baseUrl = getBaseURL()
const finalName = `${argv.name}.json`
logger.debug(`Root set to ${argv.root}`)
logger.debug(`Base Url set to ${argv.baseUrl}`)
logger.debug(`Install option set to ${argv.installLocal}`)
logger.debug(`Discard Output option set to ${argv.discardOutput}`)
logger.debug(`Invalidate Cache option set to ${argv.invalidateCache}`)
logger.debug(`Invoked generate distro name ${finalName}.`)
const doLocalInstall = argv.installLocal as boolean
const discardOutput = argv.discardOutput as boolean ?? false
const invalidateCache = argv.invalidateCache as boolean ?? false
const heliosDataFolder = getHeliosDataFolder()
if(doLocalInstall && heliosDataFolder == null) {
logger.error('You MUST specify HELIOS_DATA_FOLDER in your .env when using the --installLocal option.')
return
}
try {
const distributionStruct = new DistributionStructure(argv.root as string, argv.baseUrl as string, discardOutput, invalidateCache)
const distro = await distributionStruct.getSpecModel()
const distroOut = JSON.stringify(distro, null, 2)
const distroPath = resolvePath(argv.root as string, finalName)
await writeFile(distroPath, distroOut)
logger.info(`Successfully generated ${finalName}`)
logger.info(`Saved to ${distroPath}`)
logger.debug('Preview:\n', distro)
if(doLocalInstall) {
const finalDestination = resolvePath(heliosDataFolder!, finalName)
logger.info(`Installing distribution to ${finalDestination}`)
await writeFile(finalDestination, distroOut)
logger.info('Success!')
}
} catch (error) {
logger.error(`Failed to generate distribution with root ${argv.root}.`, error)
}
}
}
const generateSchemasCommand: CommandModule = {
command: 'schemas',
describe: 'Generate json schemas.',
handler: async (argv) => {
argv.root = getRoot()
logger.debug(`Root set to ${argv.root}`)
logger.debug('Invoked generate schemas.')
try {
await generateSchemas(argv.root as string)
logger.info('Successfully generated schemas')
} catch (error) {
logger.error(`Failed to generate schemas with root ${argv.root}.`, error)
}
}
}
const generateCommand: CommandModule = {
command: 'generate',
aliases: ['g'],
describe: 'Base generate command.',
builder: (yargs) => {
return yargs
.command(generateServerCurseForgeCommand)
.command(generateServerCommand)
.command(generateDistroCommand)
.command(generateSchemasCommand)
},
handler: (argv) => {
argv._handled = true
}
}
const validateCommand: CommandModule = {
command: 'validate [name]',
describe: 'Validate a distribution.json against the spec.',
builder: (yargs) => {
return namePositional(yargs)
},
handler: (argv) => {
logger.debug(`Invoked validate with name ${argv.name}.json`)
}
}
const latestForgeCommand: CommandModule = {
command: 'latest-forge ',
describe: 'Get the latest version of forge.',
handler: async (argv) => {
logger.debug(`Invoked latest-forge with version ${argv.version}.`)
const minecraftVersion = new MinecraftVersion(argv.version as string)
const forgeVer = await VersionUtil.getPromotedForgeVersion(minecraftVersion, 'latest')
logger.info(`Latest version: Forge ${forgeVer} (${argv.version})`)
}
}
const recommendedForgeCommand: CommandModule = {
command: 'recommended-forge ',
describe: 'Get the recommended version of forge. Returns latest if there is no recommended build.',
handler: async (argv) => {
logger.debug(`Invoked recommended-forge with version ${argv.version}.`)
const index = await VersionUtil.getPromotionIndex()
const minecraftVersion = new MinecraftVersion(argv.version as string)
let forgeVer = VersionUtil.getPromotedVersionStrict(index, minecraftVersion, 'recommended')
if (forgeVer != null) {
logger.info(`Recommended version: Forge ${forgeVer} (${minecraftVersion})`)
} else {
logger.info(`No recommended build for ${minecraftVersion}. Checking for latest version..`)
forgeVer = VersionUtil.getPromotedVersionStrict(index, minecraftVersion, 'latest')
if (forgeVer != null) {
logger.info(`Latest version: Forge ${forgeVer} (${minecraftVersion})`)
} else {
logger.info(`No build available for ${minecraftVersion}.`)
}
}
}
}
const testCommand: CommandModule = {
command: 'test ',
describe: 'Validate a distribution.json against the spec.',
builder: (yargs) => {
return namePositional(yargs)
},
handler: async (argv) => {
logger.debug(`Invoked test with mcVer ${argv.mcVer} forgeVer ${argv.forgeVer}`)
logger.info(process.cwd())
const mcVer = new MinecraftVersion(argv.mcVer as string)
const resolver = VersionSegmentedRegistry.getForgeResolver(mcVer,
argv.forgeVer as string, getRoot(), '', getBaseURL(), false, false)
if (resolver != null) {
const mdl = await resolver.getModule()
logger.info(inspect(mdl, false, null, true))
}
}
}
// Registering yargs configuration.
await yargs(hideBin(process.argv))
.version(false)
.scriptName('')
.command(initCommand)
.command(generateCommand)
.command(validateCommand)
.command(latestForgeCommand)
.command(recommendedForgeCommand)
.command(testCommand)
.demandCommand()
.help()
.argv