Reset repository to README title only

Approach is changing entirely; clearing prior implementation
to start over from a clean slate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 20:41:19 +09:00
parent 9d55819e30
commit cd79378f3c
33 changed files with 0 additions and 8451 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +0,0 @@
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('installerApi', {
getDefaults: () => ipcRenderer.invoke('installer:get-defaults'),
loadPacks: (manifestUrl: string) => ipcRenderer.invoke('installer:load-packs', manifestUrl),
inspectPack: (manifestUrl: string, packFile: string) => ipcRenderer.invoke('installer:inspect-pack', manifestUrl, packFile),
chooseDirectory: () => ipcRenderer.invoke('installer:choose-directory'),
detectJdk: (recommendedVersion?: number | null) => ipcRenderer.invoke('installer:detect-jdk', recommendedVersion),
chooseJdk: () => ipcRenderer.invoke('installer:choose-jdk'),
startInstall: (payload: unknown) => ipcRenderer.invoke('installer:start-install', payload),
acceptEula: () => ipcRenderer.invoke('installer:accept-eula'),
openConfigEditor: () => ipcRenderer.invoke('installer:open-config-editor'),
configurePort: () => ipcRenderer.invoke('installer:configure-port'),
applyClient: () => ipcRenderer.invoke('installer:apply-client'),
openFolder: () => ipcRenderer.invoke('installer:open-folder'),
createShortcut: (enabled: boolean) => ipcRenderer.invoke('installer:create-shortcut', enabled),
runServer: (enabled: boolean) => ipcRenderer.invoke('installer:run-server', enabled),
onLog: (handler: (entry: unknown) => void) => ipcRenderer.on('installer:log', (_event, entry) => handler(entry))
})

View File

@@ -1,48 +0,0 @@
import { PackDefinition, PackListEntry } from '../shared/types'
export interface InstallerDefaults {
manifestUrl: string
}
export interface PackMetadata {
packName: string
packDefinition: PackDefinition
}
export interface JdkCandidate {
path: string
majorVersion: number | null
}
export interface DetectJdkResult {
detected: string | null
candidates: JdkCandidate[]
recommendedVersion: number | null
exactMatch: boolean
}
export interface SelectedPackPayload {
manifestUrl: string
pack: PackListEntry
}
export interface InstallPayload {
manifestUrl: string
packFile: string
installPath: string
jdkPath: string
}
export interface InstallSessionState {
manifestUrl: string
baseUrl: string
packFile: string
installPath: string
jdkPath: string
packDefinition: PackDefinition
packName: string
extractedRoot: string
externalAddress?: string
externalPort?: number
configEditorUrl?: string
}

View File

@@ -1,58 +0,0 @@
import express from 'express'
import session from 'express-session'
import path from 'node:path'
import { fileDir, manifestDir, publicDir, viewsDir } from '../shared/paths'
import { ensureProjectFiles } from '../shared/store'
import { indexRouter } from './routes/index'
import { opRouter } from './routes/op'
export async function createApp() {
await ensureProjectFiles()
const app = express()
app.set('view engine', 'ejs')
app.set('views', viewsDir)
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use(
session({
secret: 'mc-custom-suite-session-secret',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: 'lax'
}
})
)
app.use('/static', express.static(publicDir))
app.use('/manifest', express.static(manifestDir))
app.use('/file', express.static(fileDir))
app.use(indexRouter)
app.use(opRouter)
app.use((error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error(error)
res.status(500).send('서버 내부 오류가 발생했습니다.')
})
return app
}
async function bootstrap() {
const app = await createApp()
const port = Number(process.env.PORT ?? 3000)
app.listen(port, '127.0.0.1', () => {
console.log(`Management site listening on http://127.0.0.1:${port}`)
})
}
if (require.main === module) {
bootstrap().catch((error) => {
console.error(error)
process.exit(1)
})
}

View File

@@ -1,15 +0,0 @@
import { NextFunction, Request, Response } from 'express'
declare module 'express-session' {
interface SessionData {
userId?: string
}
}
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
if (req.session.userId == null) {
res.redirect('/op')
return
}
next()
}

View File

@@ -1,31 +0,0 @@
import { Router } from 'express'
import path from 'node:path'
import { manifestRootPath } from '../../shared/paths'
import { fetchReleaseVersions } from '../../shared/mojang'
import { loadRootManifest } from '../../shared/store'
export const indexRouter = Router()
indexRouter.get('/', async (_req, res, next) => {
try {
const manifest = await loadRootManifest()
res.render('index', {
packs: manifest.packs
})
} catch (error) {
next(error)
}
})
indexRouter.get('/manifest.json', (_req, res) => {
res.sendFile(path.resolve(manifestRootPath))
})
indexRouter.get('/api/releases', async (_req, res, next) => {
try {
const releases = await fetchReleaseVersions()
res.json(releases)
} catch (error) {
next(error)
}
})

View File

@@ -1,293 +0,0 @@
import { Router } from 'express'
import multer from 'multer'
import path from 'node:path'
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import { fetchReleaseVersions } from '../../shared/mojang'
import { fileDir } from '../../shared/paths'
import {
createNewPack,
deletePacks,
listDashboardPacks,
loadAccounts,
loadPackDefinition,
loadRootManifest,
normalizePackDefinition,
savePackDefinition,
updatePack
} from '../../shared/store'
import { PackDefinition } from '../../shared/types'
import { requireAuth } from '../middleware/auth'
export const opRouter = Router()
const upload = multer({ storage: multer.memoryStorage() })
function pickFirstValue(value: unknown): string {
if (Array.isArray(value)) {
return typeof value[0] === 'string' ? value[0] : ''
}
return typeof value === 'string' ? value : ''
}
function sanitizeUploadFileName(name: string): string {
return name.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-')
}
function normalizeAssetPathForWeb(filePath: string): string {
return filePath.replace(/\\/g, '/')
}
async function saveUploadedPackAsset(packKey: string, bucket: 'loaders' | 'resourcepacks' | 'shaderpacks', file: Express.Multer.File): Promise<string> {
const safeName = sanitizeUploadFileName(file.originalname)
const relativePath = path.join('uploads', packKey, bucket, `${Date.now()}-${safeName}`)
const absolutePath = path.join(fileDir, relativePath)
await fsp.mkdir(path.dirname(absolutePath), { recursive: true })
await fsp.writeFile(absolutePath, file.buffer)
return normalizeAssetPathForWeb(relativePath)
}
async function mutatePackDefinition(packKey: string, mutate: (pack: PackDefinition) => void): Promise<void> {
const current = await loadPackDefinition(packKey)
if (current == null) {
throw new Error('서버팩 JSON을 찾을 수 없습니다.')
}
const next = normalizePackDefinition(current)
mutate(next)
await savePackDefinition(packKey, next)
}
async function removeUploadedAsset(relativePath: string): Promise<void> {
const absolutePath = path.join(fileDir, relativePath)
if (fs.existsSync(absolutePath)) {
await fsp.unlink(absolutePath)
}
}
opRouter.get('/op', (req, res) => {
if (req.session.userId != null) {
res.redirect('/op/dashboard')
return
}
res.render('op/login', {
errorMessage: null
})
})
opRouter.post('/op/login', async (req, res, next) => {
try {
const { password } = req.body as { password?: string }
const accounts = await loadAccounts()
const matched = accounts.find((entry) => entry.password === password)
if (matched == null) {
res.status(401).render('op/login', {
errorMessage: '비밀번호가 올바르지 않습니다.'
})
return
}
req.session.userId = matched.id
res.redirect('/op/dashboard')
} catch (error) {
next(error)
}
})
opRouter.post('/op/logout', requireAuth, (req, res) => {
req.session.destroy(() => {
res.redirect('/op')
})
})
opRouter.get('/op/dashboard', requireAuth, async (_req, res, next) => {
try {
const packs = await listDashboardPacks()
res.render('op/dashboard', {
userId: _req.session.userId,
packs
})
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/packs', requireAuth, async (_req, res, next) => {
try {
const packKey = await createNewPack()
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/packs/delete', requireAuth, async (req, res, next) => {
try {
const rawSelection = req.body.packKeys
const packKeys = Array.isArray(rawSelection)
? rawSelection.map(String)
: typeof rawSelection === 'string'
? [rawSelection]
: []
await deletePacks(packKeys)
res.redirect('/op/dashboard')
} catch (error) {
next(error)
}
})
opRouter.get('/op/dashboard/:packName', requireAuth, async (req, res, next) => {
try {
const packName = pickFirstValue(req.params.packName)
const definition = await loadPackDefinition(packName)
if (definition == null) {
res.status(404).send('서버팩 JSON을 찾을 수 없습니다.')
return
}
const rootManifest = await loadRootManifest()
const packEntry = rootManifest.packs.find((entry) => entry.file === packName)
const releases = await fetchReleaseVersions()
res.render('op/editor', {
userId: req.session.userId,
packKey: packName,
packEntry,
pack: definition,
releases
})
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName', requireAuth, async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
const nextPackName = pickFirstValue(req.body.displayName).trim() || packKey
const nextJsonKey = pickFirstValue(req.body.fileName).trim() || packKey
const currentDefinition = await loadPackDefinition(packKey)
if (currentDefinition == null) {
throw new Error('서버팩 JSON을 찾을 수 없습니다.')
}
const normalized = normalizePackDefinition({
...currentDefinition,
mcVersion: pickFirstValue(req.body.mcVersion),
recommendedJdkVersion: Number(pickFirstValue(req.body.recommendedJdkVersion)),
loaderType: pickFirstValue(req.body.loaderType) as PackDefinition['loaderType'],
loaderVersion: pickFirstValue(req.body.loaderVersion),
serverMinRam: Number(pickFirstValue(req.body.serverMinRam)),
serverMaxRam: Number(pickFirstValue(req.body.serverMaxRam)),
clientMinRam: Number(pickFirstValue(req.body.clientMinRam)),
clientRecommendedRam: Number(pickFirstValue(req.body.clientRecommendedRam)),
packPath: pickFirstValue(req.body.packPath),
description: pickFirstValue(req.body.description)
})
const changedKey = await updatePack(packKey, nextPackName, nextJsonKey, normalized)
res.redirect(`/op/dashboard/${changedKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/loader', requireAuth, upload.single('asset'), async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
if (req.file == null) {
throw new Error('업로드된 로더 파일이 없습니다.')
}
const relativePath = await saveUploadedPackAsset(packKey, 'loaders', req.file)
await mutatePackDefinition(packKey, (pack) => {
pack.loaderInstallerPath = relativePath
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/resource-pack', requireAuth, upload.single('asset'), async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
if (req.file == null) {
throw new Error('업로드된 리소스팩 파일이 없습니다.')
}
const relativePath = await saveUploadedPackAsset(packKey, 'resourcepacks', req.file)
await mutatePackDefinition(packKey, (pack) => {
pack.resourcePackFiles = [...(pack.resourcePackFiles ?? []), relativePath]
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/shader-pack', requireAuth, upload.single('asset'), async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
if (req.file == null) {
throw new Error('업로드된 쉐이더 파일이 없습니다.')
}
const relativePath = await saveUploadedPackAsset(packKey, 'shaderpacks', req.file)
await mutatePackDefinition(packKey, (pack) => {
pack.shaderPackFiles = [...(pack.shaderPackFiles ?? []), relativePath]
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/loader/remove', requireAuth, async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
const targetPath = pickFirstValue(req.body.assetPath)
await removeUploadedAsset(targetPath)
await mutatePackDefinition(packKey, (pack) => {
if (pack.loaderInstallerPath === targetPath) {
pack.loaderInstallerPath = ''
}
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/resource-pack/remove', requireAuth, async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
const targetPath = pickFirstValue(req.body.assetPath)
await removeUploadedAsset(targetPath)
await mutatePackDefinition(packKey, (pack) => {
pack.resourcePackFiles = (pack.resourcePackFiles ?? []).filter((entry) => entry !== targetPath)
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})
opRouter.post('/op/dashboard/:packName/assets/shader-pack/remove', requireAuth, async (req, res, next) => {
try {
const packKey = pickFirstValue(req.params.packName)
const targetPath = pickFirstValue(req.body.assetPath)
await removeUploadedAsset(targetPath)
await mutatePackDefinition(packKey, (pack) => {
pack.shaderPackFiles = (pack.shaderPackFiles ?? []).filter((entry) => entry !== targetPath)
})
res.redirect(`/op/dashboard/${packKey}`)
} catch (error) {
next(error)
}
})

View File

@@ -1,30 +0,0 @@
import { MinecraftRelease } from './types'
const VERSION_MANIFEST_URL = 'https://launchermeta.mojang.com/mc/game/version_manifest.json'
let cachedReleases: MinecraftRelease[] | null = null
export async function fetchReleaseVersions(): Promise<MinecraftRelease[]> {
if (cachedReleases != null) {
return cachedReleases
}
try {
const response = await fetch(VERSION_MANIFEST_URL)
if (!response.ok) {
throw new Error(`Failed to fetch versions: ${response.status}`)
}
const payload = await response.json() as { versions?: MinecraftRelease[] }
cachedReleases = (payload.versions ?? []).filter((entry) => entry.type === 'release')
return cachedReleases
} catch {
cachedReleases = [
{ id: '1.21.4', type: 'release' },
{ id: '1.21.1', type: 'release' },
{ id: '1.20.6', type: 'release' },
{ id: '1.20.1', type: 'release' }
]
return cachedReleases
}
}

View File

@@ -1,9 +0,0 @@
declare module 'nat-upnp' {
const upnp: {
createClient(): {
portMapping(options: Record<string, unknown>, callback: (error?: Error | null) => void): void
}
}
export default upnp
}

View File

@@ -1,9 +0,0 @@
import path from 'node:path'
export const projectRoot = process.cwd()
export const manifestRootPath = path.join(projectRoot, 'manifest.json')
export const accountPath = path.join(projectRoot, 'account.json')
export const manifestDir = path.join(projectRoot, 'manifest')
export const fileDir = path.join(projectRoot, 'file')
export const viewsDir = path.join(projectRoot, 'views')
export const publicDir = path.join(projectRoot, 'public')

View File

@@ -1,247 +0,0 @@
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import path from 'node:path'
import { accountPath, fileDir, manifestDir, manifestRootPath } from './paths'
import { AccountEntry, DashboardPackEntry, PackDefinition, PackListEntry, RootManifest } from './types'
const defaultRootManifest: RootManifest = {
packs: [
{
name: 'Sample Pack',
file: 'sample-pack'
}
]
}
const defaultAccount: AccountEntry[] = [
{
id: 'admin',
password: 'change-me'
}
]
const defaultPackDefinition: PackDefinition = {
mcVersion: '1.20.1',
recommendedJdkVersion: 17,
loaderType: 'vanilla',
loaderVersion: '',
loaderInstallerPath: '',
serverMinRam: 2048,
serverMaxRam: 4096,
clientMinRam: 4096,
clientRecommendedRam: 8192,
packPath: 'sample-pack.zip',
description: '새 서버팩',
configEditableFiles: ['server.properties', 'bukkit.yml'],
resourcePackFiles: [],
shaderPackFiles: []
}
async function ensureDir(targetPath: string): Promise<void> {
await fsp.mkdir(targetPath, { recursive: true })
}
async function ensureJsonFile<T>(targetPath: string, defaultValue: T): Promise<void> {
if (!fs.existsSync(targetPath)) {
await fsp.writeFile(targetPath, `${JSON.stringify(defaultValue, null, 2)}\n`, 'utf8')
}
}
export async function ensureProjectFiles(): Promise<void> {
await ensureDir(manifestDir)
await ensureDir(fileDir)
await ensureJsonFile(manifestRootPath, defaultRootManifest)
await ensureJsonFile(accountPath, defaultAccount)
const samplePackPath = path.join(manifestDir, 'sample-pack.json')
await ensureJsonFile(samplePackPath, defaultPackDefinition)
}
async function readJsonFile<T>(targetPath: string, fallback: T): Promise<T> {
try {
const raw = await fsp.readFile(targetPath, 'utf8')
return JSON.parse(raw) as T
} catch {
return fallback
}
}
async function writeJsonFile(targetPath: string, payload: unknown): Promise<void> {
await fsp.writeFile(targetPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
}
function sanitizePackKey(name: string): string {
const trimmed = name.trim().replace(/\.json$/i, '')
const normalized = trimmed.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-')
return normalized.length > 0 ? normalized : 'new'
}
export async function loadRootManifest(): Promise<RootManifest> {
await ensureProjectFiles()
return readJsonFile<RootManifest>(manifestRootPath, defaultRootManifest)
}
export async function saveRootManifest(manifest: RootManifest): Promise<void> {
await writeJsonFile(manifestRootPath, manifest)
}
export async function loadAccounts(): Promise<AccountEntry[]> {
await ensureProjectFiles()
return readJsonFile<AccountEntry[]>(accountPath, defaultAccount)
}
export async function listManifestFiles(): Promise<string[]> {
await ensureProjectFiles()
const entries = await fsp.readdir(manifestDir, { withFileTypes: true })
return entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
.map((entry) => entry.name.replace(/\.json$/i, ''))
.sort((left, right) => left.localeCompare(right))
}
export async function listDashboardPacks(): Promise<DashboardPackEntry[]> {
const [manifestFiles, rootManifest] = await Promise.all([
listManifestFiles(),
loadRootManifest()
])
return manifestFiles.map((file) => {
const registeredPack = rootManifest.packs.find((entry) => entry.file === file)
return {
file,
name: registeredPack?.name ?? file,
registered: registeredPack != null
}
})
}
export async function loadPackDefinition(packKey: string): Promise<PackDefinition | null> {
await ensureProjectFiles()
const safeKey = sanitizePackKey(packKey)
const filePath = path.join(manifestDir, `${safeKey}.json`)
if (!fs.existsSync(filePath)) {
return null
}
return readJsonFile<PackDefinition>(filePath, defaultPackDefinition)
}
export async function savePackDefinition(packKey: string, payload: PackDefinition): Promise<void> {
const safeKey = sanitizePackKey(packKey)
await writeJsonFile(path.join(manifestDir, `${safeKey}.json`), payload)
}
function nextAvailableNewKey(existing: string[]): string {
if (!existing.includes('new')) {
return 'new'
}
let index = 2
while (existing.includes(`new${index}`)) {
index += 1
}
return `new${index}`
}
export async function createNewPack(): Promise<string> {
const existing = await listManifestFiles()
const packKey = nextAvailableNewKey(existing)
await savePackDefinition(packKey, {
...defaultPackDefinition,
description: `새 서버팩 (${packKey})`
})
const manifest = await loadRootManifest()
manifest.packs.push({
name: `새 서버팩 (${packKey})`,
file: packKey
})
await saveRootManifest(manifest)
return packKey
}
export async function deletePacks(packKeys: string[]): Promise<void> {
const targetKeys = new Set(packKeys.map((entry) => sanitizePackKey(entry)))
const manifest = await loadRootManifest()
manifest.packs = manifest.packs.filter((entry) => !targetKeys.has(entry.file))
await saveRootManifest(manifest)
await Promise.all(
[...targetKeys].map(async (packKey) => {
const filePath = path.join(manifestDir, `${packKey}.json`)
if (fs.existsSync(filePath)) {
await fsp.unlink(filePath)
}
})
)
}
export async function updatePack(
currentKey: string,
nextName: string,
nextKey: string,
definition: PackDefinition
): Promise<string> {
const safeCurrentKey = sanitizePackKey(currentKey)
const safeNextKey = sanitizePackKey(nextKey)
const currentPath = path.join(manifestDir, `${safeCurrentKey}.json`)
const nextPath = path.join(manifestDir, `${safeNextKey}.json`)
if (safeCurrentKey !== safeNextKey && fs.existsSync(nextPath)) {
throw new Error('같은 이름의 JSON 파일이 이미 존재합니다.')
}
await savePackDefinition(safeNextKey, definition)
if (safeCurrentKey !== safeNextKey && fs.existsSync(currentPath)) {
await fsp.unlink(currentPath)
}
const manifest = await loadRootManifest()
const targetIndex = manifest.packs.findIndex((entry) => entry.file === safeCurrentKey)
const nextEntry: PackListEntry = {
name: nextName.trim(),
file: safeNextKey
}
if (targetIndex >= 0) {
manifest.packs[targetIndex] = nextEntry
} else {
manifest.packs.push(nextEntry)
}
await saveRootManifest(manifest)
return safeNextKey
}
export function normalizePackDefinition(input: Partial<PackDefinition>): PackDefinition {
return {
mcVersion: String(input.mcVersion ?? '1.20.1').trim() || '1.20.1',
recommendedJdkVersion: Number.isFinite(Number(input.recommendedJdkVersion))
? Number(input.recommendedJdkVersion)
: 17,
loaderType: ['vanilla', 'forge', 'fabric', 'neoforge'].includes(String(input.loaderType ?? 'vanilla'))
? String(input.loaderType ?? 'vanilla') as PackDefinition['loaderType']
: 'vanilla',
loaderVersion: String(input.loaderVersion ?? '').trim(),
loaderInstallerPath: String(input.loaderInstallerPath ?? '').trim(),
serverMinRam: Number(input.serverMinRam ?? 2048),
serverMaxRam: Number(input.serverMaxRam ?? 4096),
clientMinRam: Number(input.clientMinRam ?? 4096),
clientRecommendedRam: Number(input.clientRecommendedRam ?? 8192),
packPath: String(input.packPath ?? '').trim(),
description: String(input.description ?? '').trim(),
files: Array.isArray(input.files)
? input.files.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0)
: undefined,
configEditableFiles: Array.isArray(input.configEditableFiles)
? input.configEditableFiles.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0)
: ['server.properties', 'bukkit.yml'],
resourcePackFiles: Array.isArray(input.resourcePackFiles)
? input.resourcePackFiles.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0)
: [],
shaderPackFiles: Array.isArray(input.shaderPackFiles)
? input.shaderPackFiles.map((entry) => String(entry).trim()).filter((entry) => entry.length > 0)
: []
}
}

View File

@@ -1,40 +0,0 @@
export interface PackListEntry {
name: string
file: string
}
export interface DashboardPackEntry extends PackListEntry {
registered: boolean
}
export interface RootManifest {
packs: PackListEntry[]
}
export interface PackDefinition {
mcVersion: string
recommendedJdkVersion?: number
loaderType?: 'vanilla' | 'forge' | 'fabric' | 'neoforge'
loaderVersion?: string
loaderInstallerPath?: string
serverMinRam: number
serverMaxRam: number
clientMinRam: number
clientRecommendedRam: number
packPath: string
description?: string
files?: string[]
configEditableFiles?: string[]
resourcePackFiles?: string[]
shaderPackFiles?: string[]
}
export interface AccountEntry {
id: string
password: string
}
export interface MinecraftRelease {
id: string
type: string
}