Compare commits

..

No commits in common. "main" and "v0.1.0-dev" have entirely different histories.

26 changed files with 105 additions and 924 deletions

View File

@ -12,11 +12,6 @@ import (
"time"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/devcontainer"
"zlh-agent/internal/provision/devcontainer/go"
"zlh-agent/internal/provision/devcontainer/java"
"zlh-agent/internal/provision/devcontainer/node"
"zlh-agent/internal/provision/devcontainer/python"
"zlh-agent/internal/provision/minecraft"
"zlh-agent/internal/state"
"zlh-agent/internal/system"
@ -42,13 +37,14 @@ func runProvisionPipeline(cfg *state.Config) error {
state.SetState(state.StateInstalling)
state.SetInstallStep("provision_all")
// Installer (downloads files, patches, configs, etc.)
if err := provision.ProvisionAll(*cfg); err != nil {
state.SetError(err)
state.SetState(state.StateError)
return err
}
// Extra Minecraft verification ONLY for Minecraft
// Extra Minecraft verification
if strings.ToLower(cfg.Game) == "minecraft" {
if err := minecraft.VerifyMinecraftInstallWithRepair(*cfg); err != nil {
state.SetError(err)
@ -63,43 +59,9 @@ func runProvisionPipeline(cfg *state.Config) error {
}
/* --------------------------------------------------------------------------
ensureProvisioned() idempotent, unified
ensureProvisioned() idempotent, no goto
----------------------------------------------------------------------------*/
func ensureProvisioned(cfg *state.Config) error {
/* ---------------------------------------------------------
DEV CONTAINERS (FIRST-CLASS)
--------------------------------------------------------- */
if cfg.ContainerType == "dev" {
// If not provisioned yet, install
if !devcontainer.IsProvisioned() {
return runProvisionPipeline(cfg)
}
// Verify runtime
switch strings.ToLower(cfg.Runtime) {
case "node":
return node.Verify(*cfg)
case "python":
return python.Verify(*cfg)
case "go":
return goenv.Verify(*cfg)
case "java":
return java.Verify(*cfg)
default:
return fmt.Errorf("unsupported devcontainer runtime: %s", cfg.Runtime)
}
}
/* ---------------------------------------------------------
GAME SERVERS (EXISTING LOGIC)
--------------------------------------------------------- */
dir := provision.ServerDir(*cfg)
game := strings.ToLower(cfg.Game)
variant := strings.ToLower(cfg.Variant)
@ -120,7 +82,7 @@ func ensureProvisioned(cfg *state.Config) error {
libraries := filepath.Join(dir, "libraries")
if fileExists(runSh) && dirExists(libraries) {
return nil
return nil // already provisioned
}
return runProvisionPipeline(cfg)
@ -129,7 +91,7 @@ func ensureProvisioned(cfg *state.Config) error {
// ---------- VANILLA / PAPER / PURPUR / FABRIC ----------
jar := filepath.Join(dir, "server.jar")
if fileExists(jar) {
return nil
return nil // already provisioned
}
return runProvisionPipeline(cfg)
@ -157,6 +119,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
return
}
// Run provision + server start asynchronously
go func(c state.Config) {
log.Println("[agent] async provision+start begin")
@ -165,18 +128,15 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
return
}
// Dev containers may not start a server process
if c.ContainerType != "dev" {
if err := system.StartServer(&c); err != nil {
log.Println("[agent] start error:", err)
state.SetError(err)
state.SetState(state.StateError)
return
}
if err := system.StartServer(&c); err != nil {
log.Println("[agent] start error:", err)
state.SetError(err)
state.SetState(state.StateError)
return
}
log.Println("[agent] async provision+start complete")
}(cfg)
}(*&cfg)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
@ -184,7 +144,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
}
/* --------------------------------------------------------------------------
/start manual UI start
/start manual UI start (does NOT re-provision)
----------------------------------------------------------------------------*/
func handleStart(w http.ResponseWriter, r *http.Request) {
cfg, err := state.LoadConfig()
@ -193,11 +153,6 @@ func handleStart(w http.ResponseWriter, r *http.Request) {
return
}
if cfg.ContainerType == "dev" {
http.Error(w, "dev containers do not support manual start", http.StatusBadRequest)
return
}
if err := system.StartServer(cfg); err != nil {
http.Error(w, "start error: "+err.Error(), http.StatusInternalServerError)
return
@ -228,11 +183,6 @@ func handleRestart(w http.ResponseWriter, r *http.Request) {
return
}
if cfg.ContainerType == "dev" {
http.Error(w, "dev containers do not support restart", http.StatusBadRequest)
return
}
_ = system.StopServer()
if err := system.StartServer(cfg); err != nil {
@ -286,7 +236,7 @@ func handleSendCommand(w http.ResponseWriter, r *http.Request) {
}
/* --------------------------------------------------------------------------
Router
Router + WebSocket
----------------------------------------------------------------------------*/
func NewMux() *http.ServeMux {
m := http.NewServeMux()

View File

@ -1,35 +0,0 @@
package addons
import (
"fmt"
"zlh-agent/internal/state"
"zlh-agent/internal/provision/addons/codeserver"
)
/*
Provision installs requested addons.
IMPORTANT:
- Addons are role-agnostic (dev/game/etc)
- This function ONLY installs
- Verification happens later (ensureProvisioned)
*/
func Provision(cfg state.Config) error {
for _, addon := range cfg.Addons {
switch addon {
case "codeserver":
if err := codeserver.Install(cfg); err != nil {
return err
}
default:
return fmt.Errorf("unsupported addon: %s", addon)
}
}
return nil
}

View File

@ -1,31 +0,0 @@
package codeserver
import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers"
"zlh-agent/internal/state"
)
func Install(cfg state.Config) error {
const marker = "addon-codeserver"
if markers.IsPresent(marker) {
return nil
}
scriptPath := filepath.Join(
executil.ScriptsRoot,
"addons",
"codeserver",
"install.sh",
)
if err := executil.RunScript(scriptPath); err != nil {
return fmt.Errorf("codeserver install failed: %w", err)
}
return markers.Write(marker)
}

View File

@ -1,22 +0,0 @@
package codeserver
import (
"fmt"
"os"
"os/exec"
)
const marker = "/opt/zlh/.zlh/addons/code-server.installed"
func Verify() error {
if _, err := os.Stat(marker); err != nil {
return fmt.Errorf("code-server addon marker missing")
}
if _, err := exec.LookPath("code-server"); err != nil {
return fmt.Errorf("code-server binary not found in PATH")
}
return nil
}

View File

@ -1,24 +0,0 @@
package addons
import (
"os"
"path/filepath"
)
const (
addonMarkerDir = "/opt/zlh/.zlh/addons"
)
func IsAddonProvisioned(name string) bool {
path := filepath.Join(addonMarkerDir, name)
_, err := os.Stat(path)
return err == nil
}
func WriteAddonMarker(name string) error {
if err := os.MkdirAll(addonMarkerDir, 0755); err != nil {
return err
}
path := filepath.Join(addonMarkerDir, name)
return os.WriteFile(path, []byte("ok"), 0644)
}

View File

@ -1,56 +0,0 @@
package devcontainer
import (
"encoding/json"
"os"
"path/filepath"
"time"
)
/*
Dev container provisioning helpers.
This file is intentionally small and boring.
It exists to support idempotency and shared paths
across dev container runtimes.
*/
const (
// MarkerDir is where devcontainer state is stored.
MarkerDir = "/opt/zlh/.zlh"
// ReadyMarker is written after a dev container is fully provisioned.
ReadyMarker = "devcontainer_ready.json"
)
// ReadyMarkerPath returns the absolute path to the ready marker file.
func ReadyMarkerPath() string {
return filepath.Join(MarkerDir, ReadyMarker)
}
// IsProvisioned returns true if the dev container has already been installed.
func IsProvisioned() bool {
_, err := os.Stat(ReadyMarkerPath())
return err == nil
}
// WriteReadyMarker records successful dev container provisioning.
// This should be called by runtime installers AFTER all install steps succeed.
func WriteReadyMarker(runtime string) error {
if err := os.MkdirAll(MarkerDir, 0755); err != nil {
return err
}
data := map[string]any{
"runtime": runtime,
"ready_at": time.Now().UTC().Format(time.RFC3339),
}
raw, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
return os.WriteFile(ReadyMarkerPath(), raw, 0644)
}

View File

@ -1,30 +0,0 @@
package devcontainer
import (
"fmt"
"strings"
"zlh-agent/internal/state"
devgo "zlh-agent/internal/provision/devcontainer/go"
"zlh-agent/internal/provision/devcontainer/java"
"zlh-agent/internal/provision/devcontainer/node"
"zlh-agent/internal/provision/devcontainer/python"
)
func Provision(cfg state.Config) error {
runtime := strings.ToLower(cfg.Runtime)
switch runtime {
case "node":
return node.Install(cfg)
case "python":
return python.Install(cfg)
case "go":
return devgo.Install(cfg)
case "java":
return java.Install(cfg)
default:
return fmt.Errorf("unsupported dev container runtime: %s", runtime)
}
}

View File

@ -1,31 +0,0 @@
package goenv
import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers"
"zlh-agent/internal/state"
)
func Install(cfg state.Config) error {
const marker = "devcontainer-go"
if markers.IsPresent(marker) {
return nil
}
scriptPath := filepath.Join(
executil.ScriptsRoot,
"devcontainer",
"go",
"install.sh",
)
if err := executil.RunScript(scriptPath); err != nil {
return fmt.Errorf("go devcontainer install failed: %w", err)
}
return markers.Write(marker)
}

View File

@ -1,15 +0,0 @@
package goenv
import (
"fmt"
"os/exec"
"zlh-agent/internal/state"
)
func Verify(cfg state.Config) error {
if _, err := exec.LookPath("go"); err != nil {
return fmt.Errorf("go binary not found in PATH")
}
return nil
}

View File

@ -1,30 +0,0 @@
package java
import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers"
"zlh-agent/internal/state"
)
func Install(cfg state.Config) error {
if markers.IsPresent("devcontainer-java") {
return nil
}
scriptPath := filepath.Join(
executil.ScriptsRoot,
"devcontainer",
"java",
"install.sh",
)
if err := executil.RunScript(scriptPath); err != nil {
return fmt.Errorf("java devcontainer install failed: %w", err)
}
return markers.Write("devcontainer-java")
}

View File

@ -1,15 +0,0 @@
package java
import (
"fmt"
"os/exec"
"zlh-agent/internal/state"
)
func Verify(cfg state.Config) error {
if _, err := exec.LookPath("java"); err != nil {
return fmt.Errorf("java binary not found in PATH")
}
return nil
}

View File

@ -1,31 +0,0 @@
package node
import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers"
"zlh-agent/internal/state"
)
func Install(cfg state.Config) error {
const marker = "devcontainer-node"
if markers.IsPresent(marker) {
return nil
}
scriptPath := filepath.Join(
executil.ScriptsRoot,
"devcontainer",
"node",
"install.sh",
)
if err := executil.RunScript(scriptPath); err != nil {
return fmt.Errorf("node devcontainer install failed: %w", err)
}
return markers.Write(marker)
}

View File

@ -1,22 +0,0 @@
package node
import (
"fmt"
"os/exec"
"zlh-agent/internal/state"
)
func Verify(cfg state.Config) error {
// Version is optional at verify-time; existence is authoritative
if _, err := exec.LookPath("node"); err != nil {
return fmt.Errorf("node binary not found in PATH")
}
if _, err := exec.LookPath("npm"); err != nil {
return fmt.Errorf("npm binary not found in PATH")
}
return nil
}

View File

@ -1,31 +0,0 @@
package python
import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers"
"zlh-agent/internal/state"
)
func Install(cfg state.Config) error {
const marker = "devcontainer-python"
if markers.IsPresent(marker) {
return nil
}
scriptPath := filepath.Join(
executil.ScriptsRoot,
"devcontainer",
"python",
"install.sh",
)
if err := executil.RunScript(scriptPath); err != nil {
return fmt.Errorf("python devcontainer install failed: %w", err)
}
return markers.Write(marker)
}

View File

@ -1,18 +0,0 @@
package python
import (
"fmt"
"os/exec"
"zlh-agent/internal/state"
)
func Verify(cfg state.Config) error {
if _, err := exec.LookPath("python3"); err != nil {
return fmt.Errorf("python3 binary not found in PATH")
}
if _, err := exec.LookPath("pip3"); err != nil {
return fmt.Errorf("pip3 binary not found in PATH")
}
return nil
}

View File

@ -1,15 +0,0 @@
package executil
import (
"os"
"os/exec"
)
const ScriptsRoot = "/opt/zlh-agent/scripts"
func RunScript(path string) error {
cmd := exec.Command("/bin/bash", path)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}

View File

@ -1,22 +0,0 @@
package markers
import (
"os"
"path/filepath"
)
const (
baseDir = "/opt/zlh/.zlh"
)
func IsPresent(name string) bool {
_, err := os.Stat(filepath.Join(baseDir, name))
return err == nil
}
func Write(name string) error {
if err := os.MkdirAll(baseDir, 0755); err != nil {
return err
}
return os.WriteFile(filepath.Join(baseDir, name), []byte("ok"), 0644)
}

View File

@ -5,139 +5,117 @@ import (
"strings"
"zlh-agent/internal/state"
"zlh-agent/internal/provision/addons"
"zlh-agent/internal/provision/devcontainer"
"zlh-agent/internal/provision/minecraft"
"zlh-agent/internal/provision/steam"
)
/*
ProvisionAll unified entrypoint for provisioning.
ProvisionAll unified entrypoint for MC + Steam.
IMPORTANT:
- This function ONLY performs installation.
- Validation/verification happens in ensureProvisioned().
- state.Config is treated as immutable desired state.
*/
func ProvisionAll(cfg state.Config) error {
/* ---------------------------------------------------------
BASE ROLE PROVISIONING
--------------------------------------------------------- */
if cfg.ContainerType == "dev" {
game := strings.ToLower(cfg.Game)
variant := strings.ToLower(cfg.Variant)
if err := devcontainer.Provision(cfg); err != nil {
/* ---------------------------------------------------------
MINECRAFT
--------------------------------------------------------- */
if game == "minecraft" {
// 1. Install Java (runtime)
if err := InstallJava(cfg); err != nil {
return fmt.Errorf("java install failed: %w", err)
}
// 2. Game variant install
switch variant {
case "vanilla", "paper", "purpur", "fabric", "quilt":
if err := minecraft.InstallMinecraftVanilla(cfg); err != nil {
return fmt.Errorf("minecraft vanilla install failed: %w", err)
}
case "forge":
if err := minecraft.InstallMinecraftForge(cfg); err != nil {
return fmt.Errorf("forge install failed: %w", err)
}
case "neoforge":
if err := minecraft.InstallMinecraftNeoForge(cfg); err != nil {
return fmt.Errorf("neoforge install failed: %w", err)
}
default:
return fmt.Errorf("unsupported minecraft variant: %s", variant)
}
// 3. Config files generated AFTER variant installer
if err := WriteEula(cfg); err != nil {
return err
}
if err := WriteServerProperties(cfg); err != nil {
return err
}
if err := WriteStartScript(cfg); err != nil {
return err
}
} else {
// Legacy / default behavior assumes game containers
game := strings.ToLower(cfg.Game)
variant := strings.ToLower(cfg.Variant)
/* ---------------------------------------------------------
MINECRAFT
--------------------------------------------------------- */
if game == "minecraft" {
// 1. Install Java (runtime)
if err := InstallJava(cfg); err != nil {
return fmt.Errorf("java install failed: %w", err)
}
// 2. Game variant install
switch variant {
case "vanilla", "paper", "purpur", "fabric", "quilt":
if err := minecraft.InstallMinecraftVanilla(cfg); err != nil {
return fmt.Errorf("minecraft vanilla install failed: %w", err)
}
case "forge":
if err := minecraft.InstallMinecraftForge(cfg); err != nil {
return fmt.Errorf("forge install failed: %w", err)
}
case "neoforge":
if err := minecraft.InstallMinecraftNeoForge(cfg); err != nil {
return fmt.Errorf("neoforge install failed: %w", err)
}
default:
return fmt.Errorf("unsupported minecraft variant: %s", variant)
}
// 3. Config files generated AFTER variant installer
if err := WriteEula(cfg); err != nil {
return err
}
if err := WriteServerProperties(cfg); err != nil {
return err
}
if err := WriteStartScript(cfg); err != nil {
return err
}
} else if IsSteamGame(game) {
/* ---------------------------------------------------------
STEAM GAMES
--------------------------------------------------------- */
// 1. SteamCMD install
if err := steam.EnsureSteamCMD(); err != nil {
return fmt.Errorf("steamcmd install failed: %w", err)
}
// 2. Install game-specific content
switch game {
case "valheim":
if err := steam.InstallValheim(cfg); err != nil {
return err
}
case "rust":
if err := steam.InstallRust(cfg); err != nil {
return err
}
case "terraria":
if err := steam.InstallTerraria(cfg); err != nil {
return err
}
case "projectzomboid":
if err := steam.InstallProjectZomboid(cfg); err != nil {
return err
}
default:
return fmt.Errorf("unsupported steam game: %s", game)
}
// 3. Start script
if err := WriteStartScript(cfg); err != nil {
return err
}
} else {
return fmt.Errorf(
"unsupported container identity (containerType=%q game=%q)",
cfg.ContainerType,
cfg.Game,
)
}
// DO NOT VERIFY HERE.
// Verification happens in ensureProvisioned(), AFTER install.
return nil
}
/* ---------------------------------------------------------
ADDONS (OPTIONAL, ROLE-AGNOSTIC)
STEAM GAMES
--------------------------------------------------------- */
if len(cfg.Addons) > 0 {
if err := addons.Provision(cfg); err != nil {
if IsSteamGame(game) {
// 1. SteamCMD install
if err := steam.EnsureSteamCMD(); err != nil {
return fmt.Errorf("steamcmd install failed: %w", err)
}
// 2. Install game-specific content
switch game {
case "valheim":
if err := steam.InstallValheim(cfg); err != nil {
return err
}
case "rust":
if err := steam.InstallRust(cfg); err != nil {
return err
}
case "terraria":
if err := steam.InstallTerraria(cfg); err != nil {
return err
}
case "projectzomboid":
if err := steam.InstallProjectZomboid(cfg); err != nil {
return err
}
default:
return fmt.Errorf("unsupported steam game: %s", game)
}
// 3. Start script
if err := WriteStartScript(cfg); err != nil {
return err
}
// DO NOT VERIFY HERE (Steam verification TBD later)
return nil
}
return nil
/* ---------------------------------------------------------
UNKNOWN GAME TYPE
--------------------------------------------------------- */
return fmt.Errorf("unsupported game type: %s", game)
}

View File

@ -13,17 +13,7 @@ import (
----------------------------------------------------------------------------*/
type Config struct {
VMID int `json:"vmid"`
// Container identity
ContainerType string `json:"container_type,omitempty"`
// Dev runtime (only for dev containers)
Runtime string `json:"runtime,omitempty"`
// OPTIONAL addons (role-agnostic)
Addons []string `json:"addons,omitempty"`
VMID int `json:"vmid"`
Game string `json:"game"`
Variant string `json:"variant"`
Version string `json:"version"`
@ -42,8 +32,6 @@ type Config struct {
AdminPass string `json:"admin_pass,omitempty"`
}
/* --------------------------------------------------------------------------
AGENT STATE ENUM
----------------------------------------------------------------------------*/

View File

@ -1,60 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[code-server] starting install"
# --------------------------------------------------
# Config
# --------------------------------------------------
ADDON_ROOT="/opt/zlh/addons/code-server"
ARTIFACT_DIR="/opt/zlh/addons/code-server"
MARKER="/opt/zlh/.zlh/addons/code-server.installed"
mkdir -p "$(dirname "${MARKER}")"
# --------------------------------------------------
# Idempotency
# --------------------------------------------------
if [ -f "${MARKER}" ]; then
echo "[code-server] already installed"
exit 0
fi
ARCHIVE="$(ls ${ARTIFACT_DIR}/code-server.* 2>/dev/null | head -n1)"
if [ -z "${ARCHIVE}" ]; then
echo "[code-server][ERROR] artifact not found"
exit 1
fi
echo "[code-server] extracting ${ARCHIVE}"
mkdir -p "${ADDON_ROOT}"
TMP_DIR="$(mktemp -d)"
case "${ARCHIVE}" in
*.tar.gz)
tar -xzf "${ARCHIVE}" -C "${TMP_DIR}"
;;
*.zip)
unzip -q "${ARCHIVE}" -d "${TMP_DIR}"
;;
*)
echo "[code-server][ERROR] unsupported archive format"
exit 1
;;
esac
EXTRACTED_DIR="$(find ${TMP_DIR} -maxdepth 1 -type d -name 'code-server*' | head -n1)"
if [ -z "${EXTRACTED_DIR}" ]; then
echo "[code-server][ERROR] failed to locate extracted directory"
exit 1
fi
mv "${EXTRACTED_DIR}"/* "${ADDON_ROOT}/"
rm -rf "${TMP_DIR}"
chmod +x "${ADDON_ROOT}/bin/code-server"
touch "${MARKER}"
echo "[code-server] install complete"

View File

@ -1,51 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[go] starting go devcontainer install"
# --------------------------------------------------
# Config
# --------------------------------------------------
RUNTIME_ROOT="/opt/zlh/runtime/go"
ARTIFACT_ROOT="/opt/zlh/devcontainer/go"
VERSION="${RUNTIME_VERSION:-1.25}"
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}"
DEST_DIR="${RUNTIME_ROOT}/${VERSION}"
SYMLINK="${RUNTIME_ROOT}/current"
# --------------------------------------------------
# Guards
# --------------------------------------------------
if [ ! -d "${SRC_DIR}" ]; then
echo "[go][ERROR] artifact directory not found: ${SRC_DIR}"
exit 1
fi
if [ -x "${DEST_DIR}/bin/go" ]; then
echo "[go] go ${VERSION} already installed"
else
ARCHIVE="$(ls ${SRC_DIR}/go*.linux-amd64.tar.gz 2>/dev/null | head -n1)"
if [ -z "${ARCHIVE}" ]; then
echo "[go][ERROR] no Go archive found in ${SRC_DIR}"
exit 1
fi
echo "[go] installing go ${VERSION}"
mkdir -p "${DEST_DIR}"
tar -xzf "${ARCHIVE}" -C "${DEST_DIR}" --strip-components=1
fi
# --------------------------------------------------
# Symlink current
# --------------------------------------------------
ln -sfn "${DEST_DIR}" "${SYMLINK}"
# --------------------------------------------------
# Verify
# --------------------------------------------------
"${SYMLINK}/bin/go" version
echo "[go] go ${VERSION} install complete"

View File

@ -1,62 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[java] starting java devcontainer install"
# --------------------------------------------------
# Config
# --------------------------------------------------
RUNTIME_ROOT="/opt/zlh/runtime/java"
ARTIFACT_ROOT="/opt/zlh/devcontainer/java"
VERSION="${RUNTIME_VERSION:-17}"
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}"
DEST_DIR="${RUNTIME_ROOT}/${VERSION}"
SYMLINK="${RUNTIME_ROOT}/current"
# --------------------------------------------------
# Guards
# --------------------------------------------------
if [ ! -d "${SRC_DIR}" ]; then
echo "[java][ERROR] artifact directory not found: ${SRC_DIR}"
exit 1
fi
if [ -x "${DEST_DIR}/bin/java" ]; then
echo "[java] java ${VERSION} already installed"
else
ARCHIVE="$(ls ${SRC_DIR}/*.tar.gz 2>/dev/null | head -n1)"
if [ -z "${ARCHIVE}" ]; then
echo "[java][ERROR] no Java archive found in ${SRC_DIR}"
exit 1
fi
echo "[java] installing java ${VERSION}"
mkdir -p "${DEST_DIR}"
TMP_DIR="$(mktemp -d)"
tar -xzf "${ARCHIVE}" -C "${TMP_DIR}"
# Java archives contain a single root dir
EXTRACTED_DIR="$(find ${TMP_DIR} -maxdepth 1 -type d -name 'jdk*' | head -n1)"
if [ -z "${EXTRACTED_DIR}" ]; then
echo "[java][ERROR] failed to locate extracted jdk directory"
exit 1
fi
mv "${EXTRACTED_DIR}"/* "${DEST_DIR}/"
rm -rf "${TMP_DIR}"
fi
# --------------------------------------------------
# Symlink current
# --------------------------------------------------
ln -sfn "${DEST_DIR}" "${SYMLINK}"
# --------------------------------------------------
# Verify
# --------------------------------------------------
"${SYMLINK}/bin/java" -version
echo "[java] java ${VERSION} install complete"

View File

@ -1,40 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
RUNTIME_ROOT="/opt/zlh/runtime/node"
ARTIFACT_ROOT="/opt/zlh/devcontainer/node"
VERSION="${RUNTIME_VERSION:-24}"
ARCHIVE="node-v${VERSION}.*-linux-x64.tar.xz"
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}"
DEST_DIR="${RUNTIME_ROOT}/${VERSION}"
echo "[node] Installing Node.js version ${VERSION}"
# Ensure runtime root exists
mkdir -p "${RUNTIME_ROOT}"
# Idempotency check
if [ -d "${DEST_DIR}" ]; then
echo "[node] Node ${VERSION} already installed"
else
ARCHIVE_PATH=$(ls "${SRC_DIR}/${ARCHIVE}" 2>/dev/null || true)
if [ -z "${ARCHIVE_PATH}" ]; then
echo "[node][ERROR] Artifact not found for Node ${VERSION}"
exit 1
fi
echo "[node] Extracting ${ARCHIVE_PATH}"
mkdir -p "${DEST_DIR}"
tar -xJf "${ARCHIVE_PATH}" -C "${DEST_DIR}" --strip-components=1
fi
# Update stable symlink
ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current"
# Permissions sanity
chmod -R 755 "${DEST_DIR}"
echo "[node] Node ${VERSION} installed successfully"

View File

@ -1,72 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[python] starting python devcontainer install"
# --------------------------------------------------
# Config
# --------------------------------------------------
RUNTIME_ROOT="/opt/zlh/runtime/python"
ARTIFACT_ROOT="/opt/zlh/devcontainer/python"
VERSION="${RUNTIME_VERSION:-3.12}"
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}"
DEST_DIR="${RUNTIME_ROOT}/${VERSION}"
SYMLINK="${RUNTIME_ROOT}/current"
# --------------------------------------------------
# Guards
# --------------------------------------------------
if [ ! -d "${SRC_DIR}" ]; then
echo "[python][ERROR] artifact directory not found: ${SRC_DIR}"
exit 1
fi
if [ -x "${DEST_DIR}/bin/python3" ]; then
echo "[python] python ${VERSION} already installed"
else
ARCHIVE="$(ls ${SRC_DIR}/Python-*.tgz 2>/dev/null | head -n1)"
if [ -z "${ARCHIVE}" ]; then
echo "[python][ERROR] no Python archive found in ${SRC_DIR}"
exit 1
fi
echo "[python] building python ${VERSION}"
mkdir -p "${DEST_DIR}"
BUILD_DIR="$(mktemp -d)"
tar -xzf "${ARCHIVE}" -C "${BUILD_DIR}"
PY_SRC="$(find ${BUILD_DIR} -maxdepth 1 -type d -name 'Python-*' | head -n1)"
if [ -z "${PY_SRC}" ]; then
echo "[python][ERROR] failed to locate extracted python source"
exit 1
fi
cd "${PY_SRC}"
./configure \
--prefix="${DEST_DIR}" \
--enable-optimizations \
--with-ensurepip=install
make -j"$(nproc)"
make install
cd /
rm -rf "${BUILD_DIR}"
fi
# --------------------------------------------------
# Symlink current
# --------------------------------------------------
ln -sfn "${DEST_DIR}" "${SYMLINK}"
# --------------------------------------------------
# Verify
# --------------------------------------------------
"${SYMLINK}/bin/python3" --version
"${SYMLINK}/bin/pip3" --version
echo "[python] python ${VERSION} install complete"

View File

@ -1,22 +0,0 @@
#!/usr/bin/env bash
set -e
DEV_USER="devuser"
DEV_HOME="/home/${DEV_USER}"
WORKSPACE="${DEV_HOME}/Workspace"
echo "[workspace] ensuring dev user and workspace"
# Create user if missing
if ! id "${DEV_USER}" &>/dev/null; then
useradd -m -s /bin/bash "${DEV_USER}"
echo "[workspace] created user ${DEV_USER}"
fi
# Ensure workspace exists
mkdir -p "${WORKSPACE}"
# Ensure ownership
chown -R "${DEV_USER}:${DEV_USER}" "${DEV_HOME}"
echo "[workspace] ready (${DEV_USER}:${WORKSPACE})"

BIN
zlh-agent

Binary file not shown.