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:
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
9
src/shared/nat-upnp.d.ts
vendored
9
src/shared/nat-upnp.d.ts
vendored
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
: []
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user