Compare commits

..

3 Commits

23 changed files with 831 additions and 103 deletions

View File

@ -12,6 +12,11 @@ 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"
@ -37,14 +42,13 @@ 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
// Extra Minecraft verification ONLY for Minecraft
if strings.ToLower(cfg.Game) == "minecraft" {
if err := minecraft.VerifyMinecraftInstallWithRepair(*cfg); err != nil {
state.SetError(err)
@ -59,9 +63,43 @@ func runProvisionPipeline(cfg *state.Config) error {
}
/* --------------------------------------------------------------------------
ensureProvisioned() idempotent, no goto
ensureProvisioned() idempotent, unified
----------------------------------------------------------------------------*/
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)
@ -82,7 +120,7 @@ func ensureProvisioned(cfg *state.Config) error {
libraries := filepath.Join(dir, "libraries")
if fileExists(runSh) && dirExists(libraries) {
return nil // already provisioned
return nil
}
return runProvisionPipeline(cfg)
@ -91,7 +129,7 @@ func ensureProvisioned(cfg *state.Config) error {
// ---------- VANILLA / PAPER / PURPUR / FABRIC ----------
jar := filepath.Join(dir, "server.jar")
if fileExists(jar) {
return nil // already provisioned
return nil
}
return runProvisionPipeline(cfg)
@ -119,7 +157,6 @@ 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")
@ -128,15 +165,18 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
return
}
if err := system.StartServer(&c); err != nil {
log.Println("[agent] start error:", err)
state.SetError(err)
state.SetState(state.StateError)
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
}
}
log.Println("[agent] async provision+start complete")
}(*&cfg)
}(cfg)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
@ -144,7 +184,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
}
/* --------------------------------------------------------------------------
/start manual UI start (does NOT re-provision)
/start manual UI start
----------------------------------------------------------------------------*/
func handleStart(w http.ResponseWriter, r *http.Request) {
cfg, err := state.LoadConfig()
@ -153,6 +193,11 @@ 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
@ -183,6 +228,11 @@ 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 {
@ -236,7 +286,7 @@ func handleSendCommand(w http.ResponseWriter, r *http.Request) {
}
/* --------------------------------------------------------------------------
Router + WebSocket
Router
----------------------------------------------------------------------------*/
func NewMux() *http.ServeMux {
m := http.NewServeMux()

View File

@ -0,0 +1,35 @@
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

@ -0,0 +1,42 @@
package codeserver
import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/addons"
"zlh-agent/internal/state"
)
/*
Install installs the code-server addon.
IMPORTANT:
- Addon-only (not devcontainer-specific)
- No assumptions about users or workspaces
- Idempotent via addon marker
*/
func Install(cfg state.Config) error {
if addons.IsAddonProvisioned("codeserver") {
return nil
}
scriptPath := filepath.Join(
provision.ScriptsRoot,
"addons",
"codeserver",
"install.sh",
)
if err := provision.RunScript(scriptPath); err != nil {
return fmt.Errorf("codeserver install failed: %w", err)
}
if err := addons.WriteAddonMarker("codeserver"); err != nil {
return fmt.Errorf("failed to write codeserver marker: %w", err)
}
return nil
}

View File

@ -0,0 +1,22 @@
package codeserver
import (
"fmt"
"os/exec"
"zlh-agent/internal/state"
)
/*
Verify validates that code-server is installed and usable.
This does NOT start the service.
*/
func Verify(cfg state.Config) error {
if _, err := exec.LookPath("code-server"); err != nil {
return fmt.Errorf("code-server binary not found in PATH")
}
return nil
}

View File

@ -0,0 +1,24 @@
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

@ -0,0 +1,56 @@
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

@ -0,0 +1,57 @@
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"
)
/*
Provision entrypoint for dev container provisioning.
IMPORTANT:
- This function ONLY performs installation.
- Verification happens in ensureProvisioned().
- state.Config is treated as immutable desired state.
- Routing is based strictly on cfg.Runtime.
*/
func Provision(cfg state.Config) error {
runtime := strings.ToLower(cfg.Runtime)
switch runtime {
case "node":
if err := node.Install(cfg); err != nil {
return fmt.Errorf("node devcontainer install failed: %w", err)
}
case "python":
if err := python.Install(cfg); err != nil {
return fmt.Errorf("python devcontainer install failed: %w", err)
}
case "go":
if err := devgo.Install(cfg); err != nil {
return fmt.Errorf("go devcontainer install failed: %w", err)
}
case "java":
if err := java.Install(cfg); err != nil {
return fmt.Errorf("java devcontainer install failed: %w", err)
}
default:
return fmt.Errorf("unsupported dev container runtime: %s", runtime)
}
// DO NOT VERIFY HERE.
// Verification happens in ensureProvisioned().
return nil
}

View File

@ -0,0 +1,46 @@
package goenv
import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/devcontainer"
"zlh-agent/internal/state"
)
/*
Install installs the Go dev container runtime.
Execution model:
- Uses local, versioned install scripts from the agent repo
- Scripts handle downloading and installing Go
IMPORTANT:
- This function ONLY installs
- No verification here
*/
func Install(cfg state.Config) error {
// Idempotency guard
if devcontainer.IsProvisioned() {
return nil
}
scriptPath := filepath.Join(
provision.ScriptsRoot,
"devcontainer",
"go",
"install.sh",
)
if err := provision.RunScript(scriptPath); err != nil {
return fmt.Errorf("go devcontainer install failed: %w", err)
}
if err := devcontainer.WriteReadyMarker("go"); err != nil {
return fmt.Errorf("failed to write devcontainer ready marker: %w", err)
}
return nil
}

View File

@ -0,0 +1,36 @@
package goenv
import (
"fmt"
"os/exec"
"zlh-agent/internal/state"
)
/*
Verify validates that the Go dev container runtime is usable.
Responsibilities:
- Ensure `go` binary is present and executable
- Ensure GOPATH / GOMOD usage wont immediately fail
IMPORTANT:
- No installation here
- No state mutation
- Safe to call multiple times
*/
func Verify(cfg state.Config) error {
// Check go binary
if _, err := exec.LookPath("go"); err != nil {
return fmt.Errorf("go binary not found in PATH")
}
// Optional sanity check: go env should run
cmd := exec.Command("go", "env")
if err := cmd.Run(); err != nil {
return fmt.Errorf("go env failed to execute")
}
return nil
}

View File

@ -0,0 +1,47 @@
package java
import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/devcontainer"
"zlh-agent/internal/state"
)
/*
Install installs the Java dev container runtime.
Execution model:
- Uses local, versioned install scripts from the agent repo
- Does NOT assume Minecraft semantics
- Provides a general-purpose Java environment for development
IMPORTANT:
- This function ONLY installs
- No verification here
*/
func Install(cfg state.Config) error {
// Idempotency guard
if devcontainer.IsProvisioned() {
return nil
}
scriptPath := filepath.Join(
provision.ScriptsRoot,
"devcontainer",
"java",
"install.sh",
)
if err := provision.RunScript(scriptPath); err != nil {
return fmt.Errorf("java devcontainer install failed: %w", err)
}
if err := devcontainer.WriteReadyMarker("java"); err != nil {
return fmt.Errorf("failed to write devcontainer ready marker: %w", err)
}
return nil
}

View File

@ -0,0 +1,35 @@
package java
import (
"fmt"
"os/exec"
"zlh-agent/internal/state"
)
/*
Verify validates that the Java dev container runtime is usable.
Responsibilities:
- Ensure `java` binary is present and executable
- Ensure `javac` is present (development use case)
IMPORTANT:
- No installation here
- No state mutation
- Safe to call multiple times
*/
func Verify(cfg state.Config) error {
// Check java runtime
if _, err := exec.LookPath("java"); err != nil {
return fmt.Errorf("java binary not found in PATH")
}
// Check Java compiler (dev requirement)
if _, err := exec.LookPath("javac"); err != nil {
return fmt.Errorf("javac binary not found in PATH")
}
return nil
}

View File

@ -0,0 +1,49 @@
package node
import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/devcontainer"
"zlh-agent/internal/state"
)
/*
Install installs the Node.js dev container runtime.
Execution model:
- Uses local, versioned install scripts from the agent repo
- Scripts are responsible for fetching artifacts
- Artifact server is NOT an execution source
IMPORTANT:
- This function ONLY installs
- No verification here
*/
func Install(cfg state.Config) error {
// Idempotency guard
if devcontainer.IsProvisioned() {
return nil
}
// Local script path (inside agent repo / container)
scriptPath := filepath.Join(
provision.ScriptsRoot,
"devcontainer",
"node",
"install.sh",
)
if err := provision.RunScript(scriptPath); err != nil {
return fmt.Errorf("node devcontainer install failed: %w", err)
}
// Mark devcontainer ready
if err := devcontainer.WriteReadyMarker("node"); err != nil {
return fmt.Errorf("failed to write devcontainer ready marker: %w", err)
}
return nil
}

View File

@ -0,0 +1,35 @@
package node
import (
"fmt"
"os/exec"
"zlh-agent/internal/state"
)
/*
Verify validates that the Node.js dev container runtime is usable.
Responsibilities:
- Ensure `node` is present and executable
- Ensure `npm` is present and executable
IMPORTANT:
- No installation here
- No state mutation
- Safe to call multiple times
*/
func Verify(cfg state.Config) error {
// Check node binary
if _, err := exec.LookPath("node"); err != nil {
return fmt.Errorf("node binary not found in PATH")
}
// Check npm binary
if _, err := exec.LookPath("npm"); err != nil {
return fmt.Errorf("npm binary not found in PATH")
}
return nil
}

View File

@ -0,0 +1,46 @@
package python
import (
"fmt"
"path/filepath"
"zlh-agent/internal/provision"
"zlh-agent/internal/provision/devcontainer"
"zlh-agent/internal/state"
)
/*
Install installs the Python dev container runtime.
Execution model:
- Uses local, versioned install scripts from the agent repo
- Scripts are responsible for fetching artifacts if needed
IMPORTANT:
- This function ONLY installs
- No verification here
*/
func Install(cfg state.Config) error {
// Idempotency guard
if devcontainer.IsProvisioned() {
return nil
}
scriptPath := filepath.Join(
provision.ScriptsRoot,
"devcontainer",
"python",
"install.sh",
)
if err := provision.RunScript(scriptPath); err != nil {
return fmt.Errorf("python devcontainer install failed: %w", err)
}
if err := devcontainer.WriteReadyMarker("python"); err != nil {
return fmt.Errorf("failed to write devcontainer ready marker: %w", err)
}
return nil
}

View File

@ -0,0 +1,35 @@
package python
import (
"fmt"
"os/exec"
"zlh-agent/internal/state"
)
/*
Verify validates that the Python dev container runtime is usable.
Responsibilities:
- Ensure `python3` is present and executable
- Ensure `pip3` is present and executable
IMPORTANT:
- No installation here
- No state mutation
- Safe to call multiple times
*/
func Verify(cfg state.Config) error {
// Check python3 binary
if _, err := exec.LookPath("python3"); err != nil {
return fmt.Errorf("python3 binary not found in PATH")
}
// Check pip3 binary
if _, err := exec.LookPath("pip3"); err != nil {
return fmt.Errorf("pip3 binary not found in PATH")
}
return nil
}

View File

@ -5,117 +5,139 @@ 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 MC + Steam.
ProvisionAll unified entrypoint for provisioning.
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 {
game := strings.ToLower(cfg.Game)
variant := strings.ToLower(cfg.Variant)
/* ---------------------------------------------------------
MINECRAFT
BASE ROLE PROVISIONING
--------------------------------------------------------- */
if game == "minecraft" {
if cfg.ContainerType == "dev" {
// 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 {
if err := devcontainer.Provision(cfg); err != nil {
return err
}
// DO NOT VERIFY HERE.
// Verification happens in ensureProvisioned(), AFTER install.
return nil
} 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,
)
}
}
/* ---------------------------------------------------------
STEAM GAMES
ADDONS (OPTIONAL, ROLE-AGNOSTIC)
--------------------------------------------------------- */
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 {
if len(cfg.Addons) > 0 {
if err := addons.Provision(cfg); err != nil {
return err
}
// DO NOT VERIFY HERE (Steam verification TBD later)
return nil
}
/* ---------------------------------------------------------
UNKNOWN GAME TYPE
--------------------------------------------------------- */
return fmt.Errorf("unsupported game type: %s", game)
return nil
}

View File

@ -13,7 +13,17 @@ import (
----------------------------------------------------------------------------*/
type Config struct {
VMID int `json:"vmid"`
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"`
Game string `json:"game"`
Variant string `json:"variant"`
Version string `json:"version"`
@ -32,6 +42,8 @@ type Config struct {
AdminPass string `json:"admin_pass,omitempty"`
}
/* --------------------------------------------------------------------------
AGENT STATE ENUM
----------------------------------------------------------------------------*/

View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e
echo "[addon:codeserver] installing code-server"
# Official install script (can be replaced later if you want full control)
curl -fsSL https://code-server.dev/install.sh | sh
echo "[addon:codeserver] install complete"

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../workspace.sh"
echo "[devcontainer:go] installing golang"
apt update
apt install -y golang
echo "[devcontainer:go] install complete"

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../workspace.sh"
echo "[devcontainer:java] installing jdk"
apt update
apt install -y openjdk-21-jdk
echo "[devcontainer:java] install complete"

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../workspace.sh"
echo "[devcontainer:node] installing nodejs"
apt update
apt install -y nodejs npm
echo "[devcontainer:node] install complete"

View File

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../workspace.sh"
echo "[devcontainer:python] installing python"
apt update
apt install -y python3 python3-pip python3-venv
echo "[devcontainer:python] install complete"

22
scripts/workspace.sh Normal file
View File

@ -0,0 +1,22 @@
#!/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})"