{
    "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
    "meta": {
        "version": "PTDL_v2",
        "update_url": null
    },
    "exported_at": "2026-10-26T16:59:54+00:00",
    "name": "Eggactyl V2",
    "author": "mail@samjackaman.dev",
    "description": "Multi-egg: pick what the server runs from the console on first start. Minecraft Java (Paper, Purpur, Fabric, Forge, NeoForge, Vanilla), proxies (Velocity, BungeeCord), Bedrock, Python and Node.js bots, custom Java apps, TeamSpeak 3 and Lavalink. Or set Server Type in the Startup tab to skip the menu.",
    "features": [
        "eula"
    ],
    "docker_images": {
        "Eggactyl V2": "ghcr.io/parkervcp/yolks:debian"
    },
    "file_denylist": [],
    "startup": "bash .eggactyl/eggactyl.sh",
    "config": {
        "files": "{}",
        "startup": "{\n    \"done\": [\n        \")! For help, type\",\n        \"Done (\",\n        \"Listening on /\",\n        \"Server started.\",\n        \"Lavalink is ready to accept connections\",\n        \"[Eggactyl] Started\"\n    ]\n}",
        "logs": "{}",
        "stop": "^C"
    },
    "scripts": {
        "installation": {
            "script": "#!/bin/bash\n# Eggactyl V2 installer: writes the launcher into the server folder.\n# What the server runs is chosen from the console on first start.\n\nmkdir -p /mnt/server/.eggactyl/bin\n\n# TeamSpeak ships as .tar.bz2 and the runtime image has libbz2 but no bzip2 binary.\nif apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq --no-install-recommends bzip2 >/dev/null 2>&1; then\n    cp \"$(command -v bzip2)\" /mnt/server/.eggactyl/bin/bzip2\nelse\n    echo \"Warning: could not install bzip2 (only needed for TeamSpeak 3).\"\nfi\n\ncat > /mnt/server/.eggactyl/eggactyl.sh <<'EGGACTYL_LAUNCHER_EOF'\n#!/bin/bash\n# Eggactyl V2 launcher. Written to /home/container/.eggactyl/ by the egg's install script\n# and run on every server start. Updates itself from the latest GitHub release when it can,\n# but never needs GitHub (or anything except the official download sources) to start.\n\nEGGACTYL_VERSION=2.0.2\nUPDATE_URL=${EGGACTYL_UPDATE_URL:-https://github.com/ExecSam/Eggactyl-V2/releases/latest/download/eggactyl.sh}\n\ncd /home/container || exit 1\nEGG=/home/container/.eggactyl\nRT=$EGG/runtimes\nCONF=/home/container/eggactyl.conf\nUA=\"Eggactyl-V2/$EGGACTYL_VERSION\"\nBROWSER_UA=\"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36\"\nFILL=https://fill.papermc.io/v3/projects\nMOJANG=https://piston-meta.mojang.com/mc/game/version_manifest_v2.json\nFABRIC=https://meta.fabricmc.net/v2/versions\nM=$(uname -m)\nmkdir -p \"$EGG/bin\" \"$RT\" \"$EGG/tmp\"\nexport PATH=\"$EGG/bin:$PATH\" TMPDIR=\"$EGG/tmp\"\n\nsay()  { printf '\\033[1;36m[Eggactyl]\\033[0m %s\\n' \"$*\"; }\nwarn() { printf '\\033[1;33m[Eggactyl] %s\\033[0m\\n' \"$*\"; }\ndie()  { printf '\\033[1;31m[Eggactyl] %s\\033[0m\\n' \"$*\"; exit 1; }\nget()  { curl -fsSL --retry 3 -A \"$UA\" \"$@\"; }\ndl() { # url dest\n    say \"Downloading $(basename \"$2\")...\"\n    curl -fsSL --retry 3 -A \"$UA\" -o \"$2.part\" \"$1\" && mv -f \"$2.part\" \"$2\" && return\n    rm -f \"$2.part\"; die \"Download failed: $1\"\n}\nask() { # prompt default -> $ANSWER\n    printf '\\033[1;32m> %s\\033[0m\\n' \"$1\"\n    read -r ANSWER || die \"No console input available.\"\n    ANSWER=${ANSWER//$'\\r'/}\n    ANSWER=${ANSWER#\"${ANSWER%%[![:space:]]*}\"}; ANSWER=${ANSWER%\"${ANSWER##*[![:space:]]}\"}\n    [ -z \"$ANSWER\" ] && ANSWER=$2\n}\nconfirm() { ask \"$1 [y/N]\"; [[ \"$ANSWER\" =~ ^[Yy] ]]; }\nstarted() { echo \"[Eggactyl] Started $NAME\"; }\npick_file() { local f; for f in \"$@\"; do [ -n \"$f\" ] && [ -f \"$f\" ] && { echo \"$f\"; return 0; }; done; return 1; }\nx86_only() { [ \"$M\" = x86_64 ] || die \"$NAME only runs on x86_64 nodes (this node is $M).\"; }\n\nset_prop() { # file key value\n    touch \"$1\"\n    if grep -q \"^$2=\" \"$1\"; then sed -i \"s|^$2=.*|$2=$3|\" \"$1\"; else echo \"$2=$3\" >> \"$1\"; fi\n}\n\nself_update() { # swap in the latest released launcher if it's newer; any failure just keeps this copy\n    [ \"${LAUNCHER_UPDATE:-1}\" = 1 ] && [ -z \"$EGGACTYL_UPDATED\" ] || return\n    local new=$EGG/tmp/eggactyl.sh.new v\n    curl -fsSL --max-time 20 -A \"$UA\" -o \"$new\" \"$UPDATE_URL\" 2>/dev/null || { rm -f \"$new\"; return; }\n    v=$(sed -n 's/^EGGACTYL_VERSION=//p' \"$new\" | head -1)\n    if [ -n \"$v\" ] && [ \"$v\" != \"$EGGACTYL_VERSION\" ] && [ \"$(printf '%s\\n' \"$EGGACTYL_VERSION\" \"$v\" | sort -V | tail -1)\" = \"$v\" ] \\\n        && bash -n \"$new\" 2>/dev/null; then\n        say \"Updating Eggactyl $EGGACTYL_VERSION -> $v\"\n        chmod +x \"$new\" && mv -f \"$new\" \"$EGG/eggactyl.sh\" && EGGACTYL_UPDATED=1 exec bash \"$EGG/eggactyl.sh\"\n    fi\n    rm -f \"$new\"\n}\n\nneed_jq() {\n    command -v jq >/dev/null && return\n    local a=amd64; [ \"$M\" = aarch64 ] && a=arm64\n    dl \"https://github.com/jqlang/jq/releases/latest/download/jq-linux-$a\" \"$EGG/bin/jq\"\n    chmod +x \"$EGG/bin/jq\"\n}\n\nsave_conf() {\n    {\n        echo \"# Eggactyl V2 - delete this file and restart to run setup again.\"\n        local k; for k in TYPE VERSION BUILD JAVA JAR ARGS_FILE; do\n            [ -n \"${!k}\" ] && printf '%s=%q\\n' \"$k\" \"${!k}\"\n        done\n    } > \"$CONF\"\n}\n\n# ---------------------------------------------------------------- runtimes\n\nmc_java() { # minecraft version -> java major from Mojang's version manifest\n    local url; url=$(get \"$MOJANG\" | jq -r --arg v \"$1\" '.versions[] | select(.id == $v) | .url')\n    if [ -n \"$url\" ]; then get \"$url\" | jq -r '.javaVersion.majorVersion // 8'; else echo 21; fi\n}\n\nensure_java() {\n    local want=${JAVA_VERSION//[^0-9]/} l a=x64\n    want=${want:-${JAVA:-21}}\n    for l in 8 11 17 21 25; do [ \"$want\" -le \"$l\" ] && { want=$l; break; }; done\n    local dir=$RT/java-$want\n    if [ ! -x \"$dir/bin/java\" ]; then\n        [ \"$M\" = aarch64 ] && a=aarch64\n        dl \"https://api.adoptium.net/v3/binary/latest/$want/ga/linux/$a/jre/hotspot/normal/eclipse\" \"$EGG/tmp/java.tar.gz\"\n        rm -rf \"$dir\" && mkdir -p \"$dir\"\n        tar -xzf \"$EGG/tmp/java.tar.gz\" -C \"$dir\" --strip-components=1 || die \"Could not extract Java $want.\"\n        rm -f \"$EGG/tmp/java.tar.gz\"\n        find \"$RT\" -maxdepth 1 -name 'java-*' ! -name \"java-$want\" -exec rm -rf {} +\n    fi\n    export JAVA_HOME=$dir PATH=\"$dir/bin:$PATH\"\n    say \"Using $(java -version 2>&1 | head -1)\"\n}\n\njvm_flags() {\n    # Heap = the server's allocation minus room for the JVM's own memory (10%, 128 MB - 1 GB).\n    # Wings only adds 5-15% on top of the allocation, so sizing from the container limit gets OOM-killed.\n    local extra reserve mem=${SERVER_MEMORY:-0}; read -ra extra <<< \"$JAVA_ARGUMENTS\"\n    JFLAGS=(-Xms128M)\n    if [[ \"$JAVA_ARGUMENTS\" != *-Xmx* && \"$JAVA_ARGUMENTS\" != *MaxRAM* ]] && [ \"$mem\" -gt 0 ] 2>/dev/null; then\n        reserve=$((mem / 10)); [ \"$reserve\" -lt 128 ] && reserve=128; [ \"$reserve\" -gt 1024 ] && reserve=1024\n        JFLAGS+=(\"-Xmx$((mem - reserve))M\")\n    fi\n    JFLAGS+=(\"${extra[@]}\" -Dterminal.jline=false -Dterminal.ansi=true)\n}\n\nensure_node() {\n    local want=${NODE_VERSION#v} idx ver a=x64\n    [ \"$M\" = aarch64 ] && a=arm64\n    if idx=$(get https://nodejs.org/dist/index.json); then\n        if [ -z \"$want\" ]; then\n            ver=$(jq -r '[.[] | select(.lts)][0].version' <<< \"$idx\")\n        else\n            ver=$(jq -r --arg w \"v$want\" '[.[] | select(.version == $w or (.version | startswith($w + \".\")))][0].version // empty' <<< \"$idx\")\n        fi\n    else # offline: reuse whatever matching version is already installed\n        ver=$(cd \"$RT\" && ls -d node-v\"$want\"* 2>/dev/null | sort -V | tail -1); ver=${ver#node-}\n    fi\n    [ -z \"$ver\" ] && die \"Node.js version '$want' not found.\"\n    local dir=$RT/node-$ver\n    if [ ! -x \"$dir/bin/node\" ]; then\n        dl \"https://nodejs.org/dist/$ver/node-$ver-linux-$a.tar.xz\" \"$EGG/tmp/node.tar.xz\"\n        rm -rf \"$dir\" && mkdir -p \"$dir\"\n        tar -xJf \"$EGG/tmp/node.tar.xz\" -C \"$dir\" --strip-components=1 || die \"Could not extract Node.js $ver.\"\n        rm -f \"$EGG/tmp/node.tar.xz\"\n        find \"$RT\" -maxdepth 1 -name 'node-*' ! -name \"node-$ver\" -exec rm -rf {} +\n    fi\n    export PATH=\"$dir/bin:$PATH\"\n    say \"Using Node.js $ver\"\n}\n\nensure_uv() {\n    export UV_PYTHON_INSTALL_DIR=$RT/python UV_CACHE_DIR=$EGG/cache/uv UV_PYTHON_PREFERENCE=only-managed UV_NO_PROGRESS=1\n    [ -x \"$EGG/bin/uv\" ] && return\n    local a=x86_64; [ \"$M\" = aarch64 ] && a=aarch64\n    dl \"https://github.com/astral-sh/uv/releases/latest/download/uv-$a-unknown-linux-gnu.tar.gz\" \"$EGG/tmp/uv.tar.gz\"\n    tar -xzf \"$EGG/tmp/uv.tar.gz\" -C \"$EGG/bin\" --strip-components=1 || die \"Could not extract uv.\"\n    rm -f \"$EGG/tmp/uv.tar.gz\"\n}\n\ngit_sync() {\n    [ -z \"$GIT_REPO\" ] && return\n    local url=$GIT_REPO auth=() branch=$GIT_BRANCH\n    [[ \"$url\" == *://* || \"$url\" == git@* ]] || url=\"https://$url\"\n    [ -n \"$GIT_PAT\" ] && auth=(-c \"http.extraHeader=Authorization: Basic $(printf 'oauth2:%s' \"$GIT_PAT\" | base64 -w0)\")\n    if [ ! -d .git ]; then\n        say \"Cloning $GIT_REPO...\"\n        git init -q && git remote add origin \"$url\"\n        if [ -z \"$branch\" ]; then\n            branch=$(git \"${auth[@]}\" ls-remote --symref origin HEAD 2>/dev/null | awk '/^ref:/ {sub(\"refs/heads/\", \"\", $2); print $2}')\n        fi\n        git \"${auth[@]}\" fetch -q origin \"${branch:-main}\" && git checkout -q -f -B \"${branch:-main}\" FETCH_HEAD \\\n            || { rm -rf .git; die \"Could not clone $GIT_REPO (check the URL, branch and access token).\"; }\n        git branch -q --set-upstream-to=\"origin/${branch:-main}\" 2>/dev/null\n        printf '.eggactyl/\\neggactyl.conf\\n' >> .git/info/exclude\n    elif [ \"${AUTO_UPDATE:-1}\" = 1 ]; then\n        say \"Pulling latest changes...\"\n        git remote set-url origin \"$url\"\n        git \"${auth[@]}\" pull -q --ff-only origin \"${branch:-$(git rev-parse --abbrev-ref HEAD)}\" || warn \"git pull failed; starting with the current files.\"\n    fi\n}\n\n# ---------------------------------------------------------------- installers\n\nfill_install() { # PaperMC Fill API project (paper, velocity)\n    local p=$1 v=$VERSION b\n    if [ -z \"$v\" ] || [ \"$v\" = latest ]; then\n        for v in $(get \"$FILL/$p\" | jq -r '.versions[][] | select(test(\"-\") | not)' | head -5); do\n            b=$(get \"$FILL/$p/versions/$v/builds?channel=STABLE\" | jq -c '.[0] // empty') && [ -n \"$b\" ] && break\n        done\n    else\n        b=$(get \"$FILL/$p/versions/$v/builds?channel=STABLE\" | jq -c '.[0] // empty')\n        if [ -z \"$b\" ]; then\n            b=$(get \"$FILL/$p/versions/$v/builds/latest\") || die \"$NAME version '$v' does not exist.\"\n            warn \"No stable $NAME build for $v; using the latest experimental build.\"\n        fi\n    fi\n    [ -z \"$b\" ] && die \"Could not find a $NAME build.\"\n    VERSION=$v BUILD=$(jq -r .id <<< \"$b\") JAR=server.jar\n    JAVA=$(get \"$FILL/$p/versions/$v\" | jq -r '.version.java.version.minimum // 21')\n    # The API under-reports Velocity's minimum for some builds; Java 21+ runs all of them.\n    [ \"$p\" = velocity ] && [ \"$JAVA\" -lt 21 ] && JAVA=21\n    dl \"$(jq -r '.downloads[\"server:default\"].url' <<< \"$b\")\" server.jar\n}\n\nlatest_release() { get \"$MOJANG\" | jq -r .latest.release; }\n\ninstall_purpur() {\n    local v=$VERSION b\n    [ -z \"$v\" ] || [ \"$v\" = latest ] && v=$(get https://api.purpurmc.org/v2/purpur | jq -r '.versions[-1]')\n    b=$(get \"https://api.purpurmc.org/v2/purpur/$v\" | jq -r '.builds.latest // empty')\n    [ -z \"$b\" ] && die \"Purpur version '$v' does not exist.\"\n    VERSION=$v BUILD=$b JAR=server.jar JAVA=$(mc_java \"$v\")\n    dl \"https://api.purpurmc.org/v2/purpur/$v/$b/download\" server.jar\n}\n\ninstall_vanilla() {\n    local v=$VERSION url meta\n    [ -z \"$v\" ] || [ \"$v\" = latest ] && v=$(latest_release)\n    url=$(get \"$MOJANG\" | jq -r --arg v \"$v\" '.versions[] | select(.id == $v) | .url')\n    [ -z \"$url\" ] && die \"Minecraft version '$v' does not exist.\"\n    meta=$(get \"$url\")\n    url=$(jq -r '.downloads.server.url // empty' <<< \"$meta\")\n    [ -z \"$url\" ] && die \"Minecraft $v has no dedicated server download.\"\n    VERSION=$v JAR=server.jar JAVA=$(jq -r '.javaVersion.majorVersion // 8' <<< \"$meta\")\n    dl \"$url\" server.jar\n}\n\ninstall_fabric() {\n    local v=$VERSION loader installer\n    [ -z \"$v\" ] || [ \"$v\" = latest ] && v=$(get \"$FABRIC/game\" | jq -r '[.[] | select(.stable)][0].version')\n    loader=$(get \"$FABRIC/loader/$v\" | jq -r '.[0].loader.version // empty' 2>/dev/null)\n    [ -z \"$loader\" ] && die \"Fabric does not support Minecraft '$v'.\"\n    installer=$(get \"$FABRIC/installer\" | jq -r '[.[] | select(.stable)][0].version')\n    VERSION=$v BUILD=$loader JAR=server.jar JAVA=$(mc_java \"$v\")\n    dl \"$FABRIC/loader/$v/$loader/$installer/server/jar\" server.jar\n}\n\nrun_forge_installer() { # url\n    ensure_java\n    dl \"$1\" \"$EGG/tmp/installer.jar\"\n    say \"Running the $NAME installer (this can take a few minutes)...\"\n    java -jar \"$EGG/tmp/installer.jar\" --installServer . > \"$EGG/tmp/installer.log\" 2>&1 \\\n        || { tail -20 \"$EGG/tmp/installer.log\"; die \"$NAME installer failed.\"; }\n    rm -f \"$EGG/tmp/installer.jar\" installer.jar.log \"$EGG/tmp/installer.log\" run.bat\n}\n\ninstall_forge() {\n    local v=$VERSION promos fv\n    promos=$(get https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json)\n    [ -z \"$v\" ] || [ \"$v\" = latest ] && v=$(jq -r '.promos | keys[] | sub(\"-(latest|recommended)$\"; \"\")' <<< \"$promos\" | sort -uV | tail -1)\n    fv=$(jq -r --arg v \"$v\" '.promos[$v + \"-recommended\"] // .promos[$v + \"-latest\"] // empty' <<< \"$promos\")\n    [ -z \"$fv\" ] && die \"Forge does not support Minecraft '$v'.\"\n    VERSION=$v BUILD=$fv JAVA=$(mc_java \"$v\")\n    run_forge_installer \"https://maven.minecraftforge.net/net/minecraftforge/forge/$v-$fv/forge-$v-$fv-installer.jar\"\n    ARGS_FILE=libraries/net/minecraftforge/forge/$v-$fv/unix_args.txt JAR=\n    if [ ! -f \"$ARGS_FILE\" ]; then\n        ARGS_FILE=; local j; for j in forge-\"$v-$fv\"*.jar; do [[ \"$j\" == *installer* || ! -f \"$j\" ]] || { JAR=$j; break; }; done\n        [ -z \"$JAR\" ] && die \"Forge installed, but no server jar was found.\"\n    fi\n}\n\ninstall_neoforge() {\n    local v=$VERSION all prefix nv a b c\n    all=$(get https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml | grep -o '<version>[^<]*</version>' | sed 's/<[^>]*>//g')\n    if [ -z \"$v\" ] || [ \"$v\" = latest ]; then\n        nv=$(grep -v beta <<< \"$all\" | sort -V | tail -1)\n        IFS=. read -r a b c _ <<< \"$nv\"\n        if [ \"$a\" -lt 26 ]; then v=1.$a; [ \"$b\" != 0 ] && v=$v.$b; else v=$a.$b; [ \"$c\" != 0 ] && v=$v.$c; fi\n    else\n        IFS=. read -r a b c <<< \"${v#1.}\"\n        if [[ \"$v\" == 1.* ]]; then prefix=$a.${b:-0}.; else prefix=$a.$b.${c:-0}.; fi\n        prefix=^${prefix//./\\\\.}\n        nv=$(grep \"$prefix\" <<< \"$all\" | grep -v beta | sort -V | tail -1)\n        [ -z \"$nv\" ] && nv=$(grep \"$prefix\" <<< \"$all\" | sort -V | tail -1)\n        [ -z \"$nv\" ] && die \"NeoForge does not support Minecraft '$v' (use Forge for 1.20.1 and older).\"\n    fi\n    VERSION=$v BUILD=$nv JAVA=$(mc_java \"$v\")\n    run_forge_installer \"https://maven.neoforged.net/releases/net/neoforged/neoforge/$nv/neoforge-$nv-installer.jar\"\n    ARGS_FILE=libraries/net/neoforged/neoforge/$nv/unix_args.txt JAR=\n    [ -f \"$ARGS_FILE\" ] || die \"NeoForge installed, but $ARGS_FILE is missing.\"\n}\n\ninstall_bungeecord() {\n    VERSION=latest JAR=server.jar JAVA=21\n    dl https://ci.md-5.net/job/BungeeCord/lastSuccessfulBuild/artifact/bootstrap/target/BungeeCord.jar server.jar\n}\n\nbedrock_url() {\n    curl -fsSL --retry 3 -A \"$BROWSER_UA\" https://net-secondary.web.minecraft-services.net/api/v1.0/download/links \\\n        | jq -r '.result.links[] | select(.downloadType == \"serverBedrockLinux\") | .downloadUrl'\n}\n\ninstall_bedrock() { # $1 = download url (optional)\n    x86_only\n    local url=${1:-$(bedrock_url)} keep=() f\n    [ -z \"$url\" ] && die \"Could not find the Bedrock server download.\"\n    say \"Downloading Bedrock server $(basename \"$url\" .zip)...\"\n    curl -fsSL --retry 3 -A \"$BROWSER_UA\" -o \"$EGG/tmp/bedrock.zip\" \"$url\" || die \"Download failed: $url\"\n    for f in server.properties permissions.json allowlist.json; do [ -f \"$f\" ] && keep+=(-x \"$f\"); done\n    unzip -qo \"$EGG/tmp/bedrock.zip\" -d . \"${keep[@]}\" || die \"Could not extract the Bedrock server.\"\n    rm -f \"$EGG/tmp/bedrock.zip\"\n    chmod +x bedrock_server\n    VERSION=$(basename \"$url\" .zip); VERSION=${VERSION#bedrock-server-}\n}\n\ninstall_teamspeak() {\n    x86_only\n    command -v bzip2 >/dev/null || die \"bzip2 is missing. Reinstall the server from Settings > Reinstall Server.\"\n    local info; info=$(get https://www.teamspeak.com/versions/server.json) || die \"Could not reach teamspeak.com.\"\n    VERSION=$(jq -r .linux.x86_64.version <<< \"$info\")\n    dl \"$(jq -r '.linux.x86_64.mirrors | to_entries[0].value' <<< \"$info\")\" \"$EGG/tmp/ts3.tar.bz2\"\n    tar -xjf \"$EGG/tmp/ts3.tar.bz2\" --strip-components=1 || die \"Could not extract TeamSpeak.\"\n    rm -f \"$EGG/tmp/ts3.tar.bz2\"\n    touch .ts3server_license_accepted\n}\n\ninstall_lavalink() {\n    VERSION=latest JAR=Lavalink.jar JAVA=21\n    dl https://github.com/lavalink-devs/Lavalink/releases/latest/download/Lavalink.jar Lavalink.jar\n    if [ ! -f application.yml ]; then\n        local pw; pw=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 20)\n        printf 'server:\\n  address: 0.0.0.0\\nlavalink:\\n  server:\\n    password: \"%s\"\\n' \"$pw\" > application.yml\n        say \"Created application.yml - Lavalink password: $pw\"\n    fi\n}\n\ninstall_type() {\n    case $TYPE in\n        paper|velocity) fill_install \"$TYPE\" ;;\n        purpur|vanilla|fabric|forge|neoforge|bungeecord|bedrock|teamspeak|lavalink) \"install_$TYPE\" ;;\n    esac\n}\n\n# ---------------------------------------------------------------- setup menu\n\nTYPES=(paper purpur fabric forge neoforge vanilla velocity bungeecord bedrock python nodejs java teamspeak lavalink)\nNAMES=(Paper Purpur Fabric Forge NeoForge \"Vanilla Minecraft\" Velocity BungeeCord \"Bedrock Dedicated Server\" \"Python\" \"Node.js\" \"Java (custom .jar)\" \"TeamSpeak 3\" Lavalink)\nname_of() { local i; for i in \"${!TYPES[@]}\"; do [ \"${TYPES[$i]}\" = \"$1\" ] && echo \"${NAMES[$i]}\"; done; }\nis_versioned() { [[ \" paper purpur fabric forge neoforge vanilla velocity \" == *\" $1 \"* ]]; }\n\neula() {\n    grep -qs 'eula=true' eula.txt && return\n    say \"Minecraft servers require you to accept the Minecraft EULA: https://aka.ms/MinecraftEULA\"\n    if confirm \"Do you accept the Minecraft EULA?\"; then echo \"eula=true\" > eula.txt\n    else die \"The EULA was not accepted. Restart the server once you are ready to accept it.\"; fi\n}\n\nsetup() { # $1 = menu (ask in the console) or preset (Server Type / Server Version from the Startup tab)\n    if [ \"$1\" = menu ]; then\n        echo\n        say \"Welcome to Eggactyl V2! What should this server run?\"\n        [ -f eggactyl_config.yml ] && say \"(Eggactyl v1 config found - pick your server type once and your files are kept.)\"\n        cat <<'MENU'\n  Minecraft Java      1) Paper     2) Purpur    3) Fabric     4) Forge     5) NeoForge   6) Vanilla\n  Minecraft Proxy     7) Velocity  8) BungeeCord\n  Minecraft Bedrock   9) Bedrock Dedicated Server\n  Bots & Apps        10) Python   11) Node.js  12) Java (custom .jar)\n  Voice & Music      13) TeamSpeak 3          14) Lavalink\nMENU\n        while :; do\n            ask \"Type a number and press Enter:\"\n            if [[ \"$ANSWER\" =~ ^[0-9]+$ ]] && [ \"$ANSWER\" -ge 1 ] && [ \"$ANSWER\" -le ${#TYPES[@]} ]; then\n                TYPE=${TYPES[$((ANSWER - 1))]}; break\n            fi\n            [ -n \"$(name_of \"${ANSWER,,}\")\" ] && { TYPE=${ANSWER,,}; break; }\n            warn \"'$ANSWER' is not an option.\"\n        done\n        NAME=$(name_of \"$TYPE\")\n        say \"Selected: $NAME\"\n        VERSION=latest\n        is_versioned \"$TYPE\" && { ask \"Which version? (press Enter for latest)\" latest; VERSION=$ANSWER; }\n    else\n        TYPE=$SERVER_TYPE VERSION=${SERVER_VERSION:-latest}\n        NAME=$(name_of \"$TYPE\")\n        say \"Setting up $NAME (Server Type from the Startup tab)...\"\n    fi\n    is_versioned \"$TYPE\" || VERSION=''\n    BUILD='' JAVA='' JAR='' ARGS_FILE=''\n\n    case $TYPE in\n        paper|purpur|fabric|forge|neoforge|vanilla|bedrock) eula ;;\n        teamspeak)\n            say \"TeamSpeak license: https://www.teamspeak.com/en/privacy-and-terms/\"\n            confirm \"Do you accept the TeamSpeak license agreement?\" || die \"The license was not accepted.\" ;;\n        python|nodejs|java)\n            say \"Upload your files (or set Git Repository in the Startup tab) and set Startup File if needed.\" ;;\n    esac\n\n    install_type\n    save_conf\n    if [ \"$1\" = menu ]; then\n        say \"Setup complete. To switch later, delete eggactyl.conf and restart (or set Server Type in the Startup tab).\"\n    else\n        say \"Setup complete. To switch later, change Server Type in the Startup tab.\"\n    fi\n}\n\n# ---------------------------------------------------------------- start\n\nupdate_build() { # newer build of the *same* version for Paper / Purpur / Velocity; never changes the game version\n    [ \"${AUTO_UPDATE:-1}\" = 1 ] || return\n    local id url b\n    case $TYPE in\n        paper|velocity)\n            b=$(get \"$FILL/$TYPE/versions/$VERSION/builds?channel=STABLE\" | jq -c '.[0] // empty')\n            [ -n \"$b\" ] || return\n            id=$(jq -r .id <<< \"$b\"); url=$(jq -r '.downloads[\"server:default\"].url' <<< \"$b\") ;;\n        purpur)\n            id=$(get \"https://api.purpurmc.org/v2/purpur/$VERSION\" | jq -r '.builds.latest // empty')\n            url=\"https://api.purpurmc.org/v2/purpur/$VERSION/$id/download\" ;;\n        *) return ;;\n    esac\n    [[ \"$id\" =~ ^[0-9]+$ ]] && [ \"$id\" -gt \"${BUILD:-0}\" ] || return\n    say \"Updating $NAME $VERSION: build ${BUILD:-unknown} -> $id\"\n    if curl -fsSL --retry 3 -A \"$UA\" -o \"$JAR.part\" \"$url\"; then\n        mv -f \"$JAR.part\" \"$JAR\"; BUILD=$id; save_conf\n    else\n        rm -f \"$JAR.part\"; warn \"Update download failed; starting the current build.\"\n    fi\n}\n\nrun_console() { # stop-command cmd...\n    # Minecraft servers only reliably save when told \"stop\" (old versions, Bedrock and some mods lose\n    # data on a signal). So run the server in its own session, pass console input through, and turn\n    # the panel's stop signal (SIGINT/SIGTERM) into the server's stop command.\n    STOP_CMD=$1; shift\n    local fifo=$EGG/tmp/console.stdin pid reader rc\n    rm -f \"$fifo\" && mkfifo \"$fifo\"\n    setsid \"$@\" < \"$fifo\" &\n    pid=$!\n    exec 3> \"$fifo\" 4<&0\n    trap 'echo \"$STOP_CMD\" >&3' INT TERM\n    (trap '' INT TERM; while IFS= read -r line; do echo \"$line\" >&3; done) <&4 &\n    reader=$!\n    while :; do wait \"$pid\"; rc=$?; kill -0 \"$pid\" 2>/dev/null || break; done\n    kill -9 \"$reader\" 2>/dev/null\n    exit \"$rc\"\n}\n\nstart_minecraft() {\n    set_prop server.properties server-ip 0.0.0.0\n    set_prop server.properties server-port \"$SERVER_PORT\"\n    set_prop server.properties query.port \"$SERVER_PORT\"\n    eula\n    ensure_java; jvm_flags\n    if [ -n \"$ARGS_FILE\" ]; then\n        local user_args=(); [ -f user_jvm_args.txt ] && user_args=(@user_jvm_args.txt)\n        run_console stop java \"${JFLAGS[@]}\" \"${user_args[@]}\" \"@$ARGS_FILE\" nogui\n    fi\n    run_console stop java \"${JFLAGS[@]}\" -jar \"$JAR\" nogui\n}\n\nstart_proxy() {\n    ensure_java; jvm_flags\n    if [ \"$TYPE\" = velocity ]; then run_console shutdown java \"${JFLAGS[@]}\" -jar \"$JAR\" --port \"$SERVER_PORT\"; fi\n    if [ -f config.yml ]; then\n        sed -i -E \"s|^(\\s*-?\\s*host:\\s*).*|\\1\"\"0.0.0.0:$SERVER_PORT|; s|^(\\s*-?\\s*query_port:\\s*).*|\\1$SERVER_PORT|\" config.yml\n    else\n        printf 'listeners:\\n- host: 0.0.0.0:%s\\n  query_port: %s\\n' \"$SERVER_PORT\" \"$SERVER_PORT\" > config.yml\n    fi\n    run_console end java \"${JFLAGS[@]}\" -jar \"$JAR\"\n}\n\nstart_bedrock() {\n    if [ \"${AUTO_UPDATE:-1}\" = 1 ]; then\n        local url; url=$(bedrock_url)\n        if [ -n \"$url\" ] && [ \"$(basename \"$url\" .zip)\" != \"bedrock-server-$VERSION\" ]; then\n            say \"Updating Bedrock server $VERSION -> $(basename \"$url\" .zip | sed 's/bedrock-server-//')\"\n            install_bedrock \"$url\"; save_conf\n        fi\n    fi\n    set_prop server.properties server-port \"$SERVER_PORT\"\n    set_prop server.properties server-portv6 \"$((SERVER_PORT + 1))\"\n    export LD_LIBRARY_PATH=.\n    run_console stop ./bedrock_server\n}\n\nstart_python() {\n    git_sync\n    ensure_uv\n    local py=${PYTHON_VERSION:-3.13} venv=$EGG/venv file\n    if [ ! -x \"$venv/bin/python\" ] || [ \"$(cat \"$venv/.eggactyl-python\" 2>/dev/null)\" != \"$py\" ]; then\n        say \"Setting up Python $py...\"\n        rm -rf \"$venv\"\n        uv venv -q --seed --python \"$py\" \"$venv\" || die \"Could not install Python $py.\"\n        echo \"$py\" > \"$venv/.eggactyl-python\"\n    fi\n    export VIRTUAL_ENV=$venv PATH=\"$venv/bin:$PATH\" PYTHONUNBUFFERED=1\n    say \"Using $(python --version)\"\n    if [ -f requirements.txt ]; then\n        say \"Installing requirements.txt...\"; uv pip install -q -r requirements.txt || warn \"Installing requirements.txt failed.\"\n    elif [ -f pyproject.toml ]; then\n        say \"Installing pyproject.toml dependencies...\"; uv pip install -q -r pyproject.toml || warn \"Installing dependencies failed.\"\n    fi\n    if [ -n \"$EXTRA_PACKAGES\" ]; then\n        # shellcheck disable=SC2086\n        say \"Installing extra packages: $EXTRA_PACKAGES\"; uv pip install -q $EXTRA_PACKAGES || warn \"Installing extra packages failed.\"\n    fi\n    [ -n \"$BOT_FILENAME\" ] && [ ! -f \"$BOT_FILENAME\" ] && die \"Startup File '$BOT_FILENAME' not found.\"\n    file=$(pick_file \"$BOT_FILENAME\" main.py bot.py app.py index.py run.py) \\\n        || die \"No Python file found. Upload your files or set Git Repository, then set Startup File in the Startup tab.\"\n    started; exec python -u \"$file\"\n}\n\nstart_nodejs() {\n    git_sync\n    ensure_node\n    export NPM_CONFIG_UPDATE_NOTIFIER=false\n    if [ -f package.json ]; then\n        say \"Installing npm packages...\"; npm install --no-audit --no-fund --loglevel=error || warn \"npm install failed.\"\n    fi\n    if [ -n \"$EXTRA_PACKAGES\" ]; then\n        # shellcheck disable=SC2086\n        say \"Installing extra packages: $EXTRA_PACKAGES\"; npm install --no-audit --no-fund --no-save --loglevel=error $EXTRA_PACKAGES || warn \"Installing extra packages failed.\"\n    fi\n    if [ -n \"$BOT_FILENAME\" ]; then\n        [ -f \"$BOT_FILENAME\" ] || die \"Startup File '$BOT_FILENAME' not found.\"\n        started; exec node \"$BOT_FILENAME\"\n    fi\n    if [ -f package.json ] && [ \"$(jq -r '.scripts.start // empty' package.json)\" ]; then started; exec npm start; fi\n    local file; file=$(pick_file index.js main.js bot.js app.js index.mjs) \\\n        || die \"No Node.js file found. Upload your files or set Git Repository, then set Startup File in the Startup tab.\"\n    started; exec node \"$file\"\n}\n\nstart_java() {\n    git_sync\n    local file=$BOT_FILENAME jars\n    if [ -z \"$file\" ]; then\n        jars=$(ls ./*.jar 2>/dev/null)\n        [ \"$(echo \"$jars\" | grep -c .)\" = 1 ] && file=${jars#./}\n    fi\n    [ -n \"$file\" ] && [ -f \"$file\" ] || die \"Set Startup File to your .jar in the Startup tab (or upload exactly one .jar).\"\n    ensure_java; jvm_flags; started\n    exec java \"${JFLAGS[@]}\" -jar \"$file\"\n}\n\nstart_teamspeak() {\n    export LD_LIBRARY_PATH=\".:$LD_LIBRARY_PATH\"\n    started\n    exec ./ts3server default_voice_port=\"$SERVER_PORT\" query_port=\"${QUERY_PORT:-10011}\" filetransfer_port=\"${FILE_PORT:-30033}\" \\\n        voice_ip=0.0.0.0 query_ip=0.0.0.0 filetransfer_ip=0.0.0.0 license_accepted=1\n}\n\nstart_lavalink() {\n    ensure_java; jvm_flags\n    exec java \"${JFLAGS[@]}\" -jar \"$JAR\"\n}\n\n# ---------------------------------------------------------------- main\n\nself_update\nneed_jq\n# shellcheck disable=SC1090\n[ -f \"$CONF\" ] && source \"$CONF\"\nif [ -n \"$SERVER_TYPE\" ] && [ \"$SERVER_TYPE\" != menu ]; then\n    [ -n \"$(name_of \"$SERVER_TYPE\")\" ] || die \"Unknown Server Type '$SERVER_TYPE'.\"\n    # (Re)install when the Startup tab asks for a different type, or for a specific version we don't have.\n    if [ \"$SERVER_TYPE\" != \"$TYPE\" ] || { is_versioned \"$TYPE\" && [ -n \"$SERVER_VERSION\" ] \\\n        && [ \"$SERVER_VERSION\" != latest ] && [ \"$SERVER_VERSION\" != \"$VERSION\" ]; }; then\n        setup preset\n    fi\nelif [ ! -f \"$CONF\" ]; then\n    setup menu\nfi\nNAME=$(name_of \"$TYPE\")\n[ -z \"$NAME\" ] && { rm -f \"$CONF\"; die \"eggactyl.conf was invalid and has been removed. Restart to choose a server type.\"; }\n\n# Reinstall the server software if its files went missing (e.g. deleted by accident).\nif { [ -n \"$JAR\" ] && [ ! -f \"$JAR\" ] && [ -z \"$ARGS_FILE\" ]; } || { [ -n \"$ARGS_FILE\" ] && [ ! -f \"$ARGS_FILE\" ]; } \\\n    || { [ \"$TYPE\" = bedrock ] && [ ! -f bedrock_server ]; } || { [ \"$TYPE\" = teamspeak ] && [ ! -f ts3server ]; }; then\n    warn \"$NAME files are missing - reinstalling $VERSION.\"\n    install_type && save_conf\nfi\nupdate_build\n\nsay \"Starting $NAME${VERSION:+ $VERSION}${BUILD:+ (build $BUILD)} - Eggactyl $EGGACTYL_VERSION\"\ncase $TYPE in\n    paper|purpur|fabric|forge|neoforge|vanilla) start_minecraft ;;\n    velocity|bungeecord) start_proxy ;;\n    *) \"start_$TYPE\" ;;\nesac\nEGGACTYL_LAUNCHER_EOF\nchmod +x /mnt/server/.eggactyl/eggactyl.sh\n\necho \"Eggactyl V2 installed. Start the server and choose what it should run from the console.\"\n",
            "container": "debian:trixie-slim",
            "entrypoint": "bash"
        }
    },
    "variables": [
        {
            "name": "Server Type",
            "description": "menu = choose in the console on first start. Pick a type to set the server up without the menu; changing it later reinstalls the server software (your files are kept).",
            "env_variable": "SERVER_TYPE",
            "default_value": "menu",
            "user_viewable": true,
            "user_editable": true,
            "rules": "required|string|in:menu,paper,purpur,fabric,forge,neoforge,vanilla,velocity,bungeecord,bedrock,python,nodejs,java,teamspeak,lavalink",
            "field_type": "text"
        },
        {
            "name": "Server Version",
            "description": "Minecraft / Velocity version used with Server Type, e.g. 1.21.4. latest = newest at setup time (the game version is never upgraded automatically; set a version here to change it).",
            "env_variable": "SERVER_VERSION",
            "default_value": "latest",
            "user_viewable": true,
            "user_editable": true,
            "rules": "required|string|max:40",
            "field_type": "text"
        },
        {
            "name": "Startup File",
            "description": "File to run for Python / Node.js / custom Java (e.g. bot.py, index.js, app.jar). Blank = auto-detect.",
            "env_variable": "BOT_FILENAME",
            "default_value": "",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|string|max:255",
            "field_type": "text"
        },
        {
            "name": "Git Repository",
            "description": "Optional. Git URL to clone your bot/app from (Python, Node.js, custom Java).",
            "env_variable": "GIT_REPO",
            "default_value": "",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|string|max:400",
            "field_type": "text"
        },
        {
            "name": "Git Branch",
            "description": "Branch to use. Blank = the repository's default branch.",
            "env_variable": "GIT_BRANCH",
            "default_value": "",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|string|max:255",
            "field_type": "text"
        },
        {
            "name": "Git Access Token",
            "description": "Personal access token for private repositories. Never written to disk.",
            "env_variable": "GIT_PAT",
            "default_value": "",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|string|max:2000",
            "field_type": "text"
        },
        {
            "name": "Auto Update",
            "description": "1 = on start: git pull (bots), newest Paper/Purpur/Velocity build for the same game version, latest Bedrock release. 0 = off.",
            "env_variable": "AUTO_UPDATE",
            "default_value": "1",
            "user_viewable": true,
            "user_editable": true,
            "rules": "required|boolean",
            "field_type": "text"
        },
        {
            "name": "Additional Packages",
            "description": "Extra pip (Python) or npm (Node.js) packages to install on start, space separated.",
            "env_variable": "EXTRA_PACKAGES",
            "default_value": "",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|string|max:1000",
            "field_type": "text"
        },
        {
            "name": "Python Version",
            "description": "Python version for Python bots, e.g. 3.13 or 3.12.",
            "env_variable": "PYTHON_VERSION",
            "default_value": "3.13",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|regex:/^3\\.[0-9]+(\\.[0-9]+)?$/",
            "field_type": "text"
        },
        {
            "name": "Node.js Version",
            "description": "Node.js version, e.g. 22 or 24.1.0. Blank = latest LTS.",
            "env_variable": "NODE_VERSION",
            "default_value": "",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|regex:/^v?[0-9]+(\\.[0-9]+){0,2}$/",
            "field_type": "text"
        },
        {
            "name": "Java Version",
            "description": "Force a Java version (8, 11, 17, 21, 25). Blank = automatic for the chosen server.",
            "env_variable": "JAVA_VERSION",
            "default_value": "",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|regex:/^[0-9]{1,2}$/",
            "field_type": "text"
        },
        {
            "name": "Java Arguments",
            "description": "Extra JVM flags. Heap defaults to the server's memory minus 10% (128 MB-1 GB) for the JVM itself, unless you pass -Xmx.",
            "env_variable": "JAVA_ARGUMENTS",
            "default_value": "",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|string|max:2000",
            "field_type": "text"
        },
        {
            "name": "Query Port",
            "description": "TeamSpeak 3 ServerQuery port (must be an allocation on this server).",
            "env_variable": "QUERY_PORT",
            "default_value": "10011",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|integer|between:1,65535",
            "field_type": "text"
        },
        {
            "name": "File Port",
            "description": "TeamSpeak 3 file transfer port (must be an allocation on this server).",
            "env_variable": "FILE_PORT",
            "default_value": "30033",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|integer|between:1,65535",
            "field_type": "text"
        },
        {
            "name": "Eggactyl Self-Update",
            "description": "1 = update the Eggactyl launcher from the latest GitHub release on start (keeps the installed copy if GitHub is unreachable). Admin only.",
            "env_variable": "LAUNCHER_UPDATE",
            "default_value": "1",
            "user_viewable": true,
            "user_editable": false,
            "rules": "required|boolean",
            "field_type": "text"
        }
    ]
}
