chore: bootstrap lean sysadmin-chronicles repo
Import the runnable game code, content, docs, scripts, and repo guidance while leaving local agent state, dependency installs, build output, and backup copies out of the published tree.
This commit is contained in:
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install config management for Sysadmin Chronicles.
|
||||
# Config lives at ~/.config/sysadmin-chronicles/config (survives game dir moves).
|
||||
# Source this file; do not execute directly.
|
||||
|
||||
SC_CONFIG_DIR="${SC_CONFIG_DIR:-$HOME/.config/sysadmin-chronicles}"
|
||||
SC_CONFIG_FILE="$SC_CONFIG_DIR/config"
|
||||
|
||||
config_read() {
|
||||
[ -f "$SC_CONFIG_FILE" ] && source "$SC_CONFIG_FILE" || true
|
||||
}
|
||||
|
||||
config_write() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
mkdir -p "$SC_CONFIG_DIR"
|
||||
local tmp
|
||||
tmp="$(mktemp "$SC_CONFIG_DIR/config.XXXXXX")"
|
||||
if [ -f "$SC_CONFIG_FILE" ]; then
|
||||
awk -v key="$key" -v value="$value" '
|
||||
BEGIN { found = 0 }
|
||||
index($0, key "=") == 1 {
|
||||
print key "=" value
|
||||
found = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!found) {
|
||||
print key "=" value
|
||||
}
|
||||
}
|
||||
' "$SC_CONFIG_FILE" > "$tmp"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$value" > "$tmp"
|
||||
fi
|
||||
mv "$tmp" "$SC_CONFIG_FILE"
|
||||
}
|
||||
|
||||
config_show() {
|
||||
if [ ! -f "$SC_CONFIG_FILE" ]; then
|
||||
echo " (no config file at $SC_CONFIG_FILE)"
|
||||
return
|
||||
fi
|
||||
echo " Config: $SC_CONFIG_FILE"
|
||||
local line key value
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ ^# ]] && continue
|
||||
[[ -z "$line" ]] && continue
|
||||
key="${line%%=*}"
|
||||
value="${line#*=}"
|
||||
printf ' %-28s %s\n' "$key" "$value"
|
||||
done < "$SC_CONFIG_FILE"
|
||||
}
|
||||
Executable
+169
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env bash
|
||||
# Dependency detection and installation for Sysadmin Chronicles.
|
||||
# Source this file; do not execute directly.
|
||||
|
||||
SC_DISTRO=""
|
||||
|
||||
detect_distro() {
|
||||
if [ -f /etc/arch-release ]; then
|
||||
SC_DISTRO=arch
|
||||
elif grep -qi ubuntu /etc/os-release 2>/dev/null; then
|
||||
SC_DISTRO=ubuntu
|
||||
elif [ -f /etc/debian_version ]; then
|
||||
SC_DISTRO=debian
|
||||
elif [ -f /etc/fedora-release ]; then
|
||||
SC_DISTRO=fedora
|
||||
elif grep -qi opensuse /etc/os-release 2>/dev/null; then
|
||||
SC_DISTRO=opensuse
|
||||
else
|
||||
SC_DISTRO=unknown
|
||||
fi
|
||||
}
|
||||
|
||||
# Per-distro package names for canonical dep names.
|
||||
# Format: canonical:arch:debian:ubuntu:fedora:opensuse
|
||||
# Empty field = not applicable / same as above / handled specially.
|
||||
_SC_DEP_MAP=(
|
||||
"libvirt:libvirt:libvirt-daemon-system:libvirt-daemon-system:libvirt:libvirt"
|
||||
"qemu-system:qemu-system-x86:qemu-system-x86:qemu-kvm:qemu-kvm:qemu-kvm"
|
||||
"qemu-img:qemu-img:qemu-utils:qemu-utils:qemu-img:qemu-tools"
|
||||
"virt-install:virt-install:virtinst:virtinst:virt-install:virt-install"
|
||||
"virt-viewer:virt-viewer:virt-viewer:virt-viewer:virt-viewer:virt-viewer"
|
||||
"cloud-localds:cloud-image-utils:cloud-image-utils:cloud-image-utils:cloud-utils:cloud-utils"
|
||||
"genisoimage:cdrtools:genisoimage:genisoimage:genisoimage:genisoimage"
|
||||
"xorriso:libisoburn:xorriso:xorriso:xorriso:xorriso"
|
||||
"nodejs:nodejs:nodejs:nodejs:nodejs:nodejs"
|
||||
"openssh:openssh:openssh-client:openssh-client:openssh-clients:openssh-clients"
|
||||
)
|
||||
|
||||
# Arch-only QEMU SPICE/display extras installed alongside qemu-system
|
||||
_SC_ARCH_QEMU_EXTRAS=(
|
||||
qemu-hw-display-qxl
|
||||
qemu-hw-display-virtio-gpu
|
||||
qemu-ui-spice-core
|
||||
qemu-chardev-spice
|
||||
qemu-audio-spice
|
||||
)
|
||||
|
||||
map_package() {
|
||||
local dep="$1"
|
||||
local distro="${2:-$SC_DISTRO}"
|
||||
local entry
|
||||
for entry in "${_SC_DEP_MAP[@]}"; do
|
||||
IFS=':' read -r name arch debian ubuntu fedora opensuse <<< "$entry"
|
||||
if [ "$name" = "$dep" ]; then
|
||||
case "$distro" in
|
||||
arch) printf '%s' "$arch" ;;
|
||||
debian) printf '%s' "$debian" ;;
|
||||
ubuntu) printf '%s' "$ubuntu" ;;
|
||||
fedora) printf '%s' "$fedora" ;;
|
||||
opensuse) printf '%s' "$opensuse" ;;
|
||||
esac
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Outputs canonical dep names that are not yet installed (one per line)
|
||||
check_deps() {
|
||||
local missing=()
|
||||
command -v virsh >/dev/null 2>&1 || missing+=(libvirt)
|
||||
command -v qemu-system-x86_64 >/dev/null 2>&1 || missing+=(qemu-system)
|
||||
command -v qemu-img >/dev/null 2>&1 || missing+=(qemu-img)
|
||||
command -v virt-install >/dev/null 2>&1 || missing+=(virt-install)
|
||||
command -v remote-viewer >/dev/null 2>&1 || missing+=(virt-viewer)
|
||||
command -v node >/dev/null 2>&1 || missing+=(nodejs)
|
||||
command -v ssh >/dev/null 2>&1 || missing+=(openssh)
|
||||
# Need at least one cloud-init ISO tool
|
||||
if ! command -v cloud-localds >/dev/null 2>&1 \
|
||||
&& ! command -v genisoimage >/dev/null 2>&1 \
|
||||
&& ! command -v mkisofs >/dev/null 2>&1 \
|
||||
&& ! command -v xorriso >/dev/null 2>&1; then
|
||||
missing+=(cloud-localds genisoimage xorriso)
|
||||
fi
|
||||
[ "${#missing[@]}" -gt 0 ] && printf '%s\n' "${missing[@]}" || true
|
||||
}
|
||||
|
||||
# Install a list of canonical dep names. Logs to SC_INSTALL_LOG if set.
|
||||
install_deps() {
|
||||
local deps=("$@")
|
||||
local pkgs=()
|
||||
|
||||
for dep in "${deps[@]}"; do
|
||||
local pkg
|
||||
pkg="$(map_package "$dep")"
|
||||
[ -n "$pkg" ] && pkgs+=("$pkg")
|
||||
done
|
||||
|
||||
if [ "$SC_DISTRO" = "arch" ]; then
|
||||
pkgs+=("${_SC_ARCH_QEMU_EXTRAS[@]}")
|
||||
fi
|
||||
|
||||
[ "${#pkgs[@]}" -eq 0 ] && return 0
|
||||
|
||||
local -a pm_cmd
|
||||
case "$SC_DISTRO" in
|
||||
arch) pm_cmd=(pacman -S --noconfirm --needed) ;;
|
||||
debian|ubuntu) pm_cmd=(apt-get install -y) ;;
|
||||
fedora) pm_cmd=(dnf install -y) ;;
|
||||
opensuse) pm_cmd=(zypper install -y) ;;
|
||||
*)
|
||||
echo " Unsupported distro '$SC_DISTRO' — install these manually:"
|
||||
printf ' %s\n' "${pkgs[@]}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
sudo "${pm_cmd[@]}" "${pkgs[@]}"
|
||||
|
||||
if [ -n "${SC_INSTALL_LOG:-}" ] && [ "$SC_INSTALL_LOG" != "/dev/null" ]; then
|
||||
mkdir -p "$(dirname "$SC_INSTALL_LOG")"
|
||||
local distro_label="$SC_DISTRO"
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
local ver=""
|
||||
case "$SC_DISTRO" in
|
||||
arch) ver="$(pacman -Q "$pkg" 2>/dev/null | awk '{print $2}' || true)" ;;
|
||||
debian|ubuntu) ver="$(dpkg -l "$pkg" 2>/dev/null | awk '/^ii/{print $3}' | head -1 || true)" ;;
|
||||
fedora) ver="$(rpm -q --queryformat '%{VERSION}' "$pkg" 2>/dev/null || true)" ;;
|
||||
esac
|
||||
printf '[INSTALLED] %-36s %-14s via %s\n' "$pkg" "${ver:-}" "$distro_label" \
|
||||
>> "$SC_INSTALL_LOG"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Outputs already-installed canonical deps (for log completeness)
|
||||
log_present_deps() {
|
||||
local log_file="${SC_INSTALL_LOG:-}"
|
||||
[ -z "$log_file" ] || [ "$log_file" = "/dev/null" ] && return
|
||||
local distro_label="$SC_DISTRO"
|
||||
for dep in libvirt qemu-system qemu-img virt-install virt-viewer nodejs openssh; do
|
||||
local bin
|
||||
case "$dep" in
|
||||
libvirt) bin=virsh ;;
|
||||
qemu-system) bin=qemu-system-x86_64 ;;
|
||||
qemu-img) bin=qemu-img ;;
|
||||
virt-install) bin=virt-install ;;
|
||||
virt-viewer) bin=remote-viewer ;;
|
||||
nodejs) bin=node ;;
|
||||
openssh) bin=ssh ;;
|
||||
esac
|
||||
if command -v "$bin" >/dev/null 2>&1; then
|
||||
printf '[SKIPPED] %-36s already installed\n' "$dep" >> "$log_file"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
dep_label() {
|
||||
case "$1" in
|
||||
libvirt) echo "Virtual machine manager (libvirt)" ;;
|
||||
qemu-system) echo "KVM virtualization support (QEMU)" ;;
|
||||
qemu-img) echo "VM disk image tools (qemu-img)" ;;
|
||||
virt-install) echo "VM installer (virt-install)" ;;
|
||||
virt-viewer) echo "SPICE display viewer (virt-viewer)" ;;
|
||||
cloud-localds|genisoimage|xorriso) echo "Cloud image tools" ;;
|
||||
nodejs) echo "Node.js runtime" ;;
|
||||
openssh) echo "SSH client" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared internal HTTPS/URL helpers for Sysadmin Chronicles launch and VM build scripts.
|
||||
# Source this file; do not execute directly.
|
||||
|
||||
sc_internal_port() {
|
||||
printf '%s\n' "${PORT:-3000}"
|
||||
}
|
||||
|
||||
sc_cert_dir() {
|
||||
printf '%s\n' "${SC_CERT_DIR:-$HOME/.local/share/sysadmin-chronicles/certs}"
|
||||
}
|
||||
|
||||
sc_tls_cert() {
|
||||
printf '%s/server.crt\n' "$(sc_cert_dir)"
|
||||
}
|
||||
|
||||
sc_tls_key() {
|
||||
printf '%s/server.key\n' "$(sc_cert_dir)"
|
||||
}
|
||||
|
||||
sc_ca_cert() {
|
||||
printf '%s/ca.crt\n' "$(sc_cert_dir)"
|
||||
}
|
||||
|
||||
sc_hud_url() {
|
||||
printf '%s\n' "${SC_HUD_URL:-https://portal.axiomworks.internal:$(sc_internal_port)}"
|
||||
}
|
||||
|
||||
sc_sage_url() {
|
||||
printf '%s\n' "${SC_SAGE_URL:-https://sage.axiomworks.internal:$(sc_internal_port)/sage/}"
|
||||
}
|
||||
|
||||
sc_company_url() {
|
||||
printf '%s\n' "${SC_COMPANY_URL:-https://www.axiomworks.corp/}"
|
||||
}
|
||||
|
||||
sc_have_internal_certs() {
|
||||
[[ -f "$(sc_tls_cert)" && -f "$(sc_tls_key)" && -f "$(sc_ca_cert)" ]]
|
||||
}
|
||||
|
||||
sc_ensure_internal_certs() {
|
||||
local project_root="$1"
|
||||
if sc_have_internal_certs; then
|
||||
return 0
|
||||
fi
|
||||
bash "$project_root/tools/setup/generate-certs.sh"
|
||||
}
|
||||
|
||||
sc_export_internal_https_env() {
|
||||
export SC_CERT_DIR="$(sc_cert_dir)"
|
||||
export SC_TLS_CERT="$(sc_tls_cert)"
|
||||
export SC_TLS_KEY="$(sc_tls_key)"
|
||||
export SC_HUD_URL="$(sc_hud_url)"
|
||||
export SC_SAGE_URL="$(sc_sage_url)"
|
||||
export SC_COMPANY_URL="$(sc_company_url)"
|
||||
}
|
||||
|
||||
sc_listen_pids() {
|
||||
local port="$1"
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | sort -u
|
||||
return 0
|
||||
fi
|
||||
ss -H -ltnp "sport = :$port" 2>/dev/null \
|
||||
| sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' \
|
||||
| sort -u
|
||||
}
|
||||
|
||||
sc_pid_is_repo_server() {
|
||||
local pid="$1"
|
||||
local project_root="$2"
|
||||
local server_dir="$project_root/server"
|
||||
local cwd=""
|
||||
local cmdline=""
|
||||
|
||||
[[ -r "/proc/$pid/cmdline" ]] || return 1
|
||||
cwd="$(readlink -f "/proc/$pid/cwd" 2>/dev/null || true)"
|
||||
cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)"
|
||||
|
||||
[[ "$cwd" == "$server_dir" ]] || return 1
|
||||
[[ "$cmdline" == *"node"* && "$cmdline" == *"src/index.js"* ]]
|
||||
}
|
||||
|
||||
sc_pid_has_internal_tls() {
|
||||
local pid="$1"
|
||||
[[ -r "/proc/$pid/environ" ]] || return 1
|
||||
tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null \
|
||||
| grep -q '^SC_TLS_CERT=.*server\.crt$' \
|
||||
&& tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null \
|
||||
| grep -q '^SC_TLS_KEY=.*server\.key$'
|
||||
}
|
||||
|
||||
sc_stop_pid() {
|
||||
local pid="$1"
|
||||
kill "$pid" 2>/dev/null || true
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||
kill -0 "$pid" 2>/dev/null || return 0
|
||||
sleep 0.2
|
||||
done
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
}
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
# libvirt wrappers for Sysadmin Chronicles.
|
||||
# Source this file; do not execute directly.
|
||||
# Expects LIBVIRT_DEFAULT_URI to be set by the caller.
|
||||
#
|
||||
# SC_VIRSH_SUDO=true — prefix all virsh calls with sudo.
|
||||
# Set this in install.sh when the current session doesn't yet have
|
||||
# the libvirt group active (e.g., right after usermod -aG libvirt).
|
||||
|
||||
_virsh() {
|
||||
if [ "${SC_VIRSH_SUDO:-false}" = true ]; then
|
||||
sudo virsh "$@"
|
||||
else
|
||||
virsh "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_network() {
|
||||
local name="$1"
|
||||
local xml_path="$2"
|
||||
if _virsh net-list --all 2>/dev/null | grep -q "\\b${name}\\b"; then
|
||||
if ! _virsh net-info "$name" 2>/dev/null | grep -q "Active:.*yes"; then
|
||||
_virsh net-start "$name" >/dev/null 2>&1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
_virsh net-define "$xml_path" >/dev/null 2>&1
|
||||
_virsh net-autostart "$name" >/dev/null 2>&1
|
||||
_virsh net-start "$name" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
ensure_pool() {
|
||||
local name="$1"
|
||||
local path="$2"
|
||||
if _virsh pool-list --all 2>/dev/null | grep -q "\\b${name}\\b"; then
|
||||
if ! _virsh pool-info "$name" 2>/dev/null | grep -q "State:.*running"; then
|
||||
_virsh pool-start "$name" >/dev/null 2>&1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$path"
|
||||
_virsh pool-define-as "$name" dir --target "$path" >/dev/null 2>&1
|
||||
_virsh pool-autostart "$name" >/dev/null 2>&1
|
||||
_virsh pool-start "$name" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
pool_path() {
|
||||
local name="$1"
|
||||
_virsh pool-dumpxml "$name" 2>/dev/null \
|
||||
| sed -n 's:.*<path>\(.*\)</path>.*:\1:p' \
|
||||
| head -n1
|
||||
}
|
||||
|
||||
domain_exists() {
|
||||
_virsh dominfo "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
domain_state() {
|
||||
_virsh domstate "$1" 2>/dev/null | tr -d ' \n'
|
||||
}
|
||||
|
||||
network_active() {
|
||||
_virsh net-info "$1" 2>/dev/null | grep -q "Active:.*yes"
|
||||
}
|
||||
|
||||
ensure_network_active() {
|
||||
local name="$1"
|
||||
_virsh net-list --all 2>/dev/null | grep -q "\\b${name}\\b" || return 1
|
||||
network_active "$name" || _virsh net-start "$name" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
snapshot_exists() {
|
||||
_virsh snapshot-info "$1" "$2" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
snapshot_create() {
|
||||
local domain="$1"
|
||||
local name="$2"
|
||||
local desc="${3:-}"
|
||||
_virsh snapshot-delete "$domain" "$name" >/dev/null 2>&1 || true
|
||||
_virsh snapshot-create-as "$domain" "$name" --description "$desc" --atomic
|
||||
}
|
||||
|
||||
snapshot_revert() {
|
||||
_virsh snapshot-revert "$1" "$2" --running
|
||||
}
|
||||
|
||||
snapshot_delete() {
|
||||
_virsh snapshot-delete "$1" "$2"
|
||||
}
|
||||
|
||||
snapshot_list_names() {
|
||||
_virsh snapshot-list "$1" --name 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Returns approximate qcow2 disk usage for a domain in human-readable form
|
||||
domain_disk_usage() {
|
||||
local domain="$1"
|
||||
local total=0
|
||||
local disk
|
||||
for disk in $(_virsh domblklist "$domain" --details 2>/dev/null | awk '/disk/ && $4 != "-" {print $4}' || true); do
|
||||
[ -f "$disk" ] || continue
|
||||
local sz
|
||||
sz="$(du -sb "$disk" 2>/dev/null | awk '{print $1}' || echo 0)"
|
||||
total=$(( total + sz ))
|
||||
done
|
||||
if [ "$total" -gt 0 ]; then
|
||||
numfmt --to=iec-i --suffix=B "$total" 2>/dev/null || echo "${total}B"
|
||||
else
|
||||
echo "0B"
|
||||
fi
|
||||
}
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# Save slot management for Sysadmin Chronicles.
|
||||
# Source this file; do not execute directly.
|
||||
|
||||
SC_SAVE_DIR="${SC_SAVE_DIR:-$HOME/.local/share/sysadmin-chronicles/saves}"
|
||||
|
||||
_save_path() { printf '%s/%s.json' "$SC_SAVE_DIR" "$1"; }
|
||||
|
||||
_save_active_slot() {
|
||||
local slot=""
|
||||
if [ -f "$SC_SAVE_DIR/.active" ]; then
|
||||
slot="$(cat "$SC_SAVE_DIR/.active")"
|
||||
fi
|
||||
printf '%s' "${slot:-autosave}"
|
||||
}
|
||||
|
||||
_save_valid_slot() {
|
||||
case "$1" in
|
||||
autosave|slot-1|slot-2|slot-3) return 0 ;;
|
||||
*) echo " ✗ Invalid slot name: $1 (use autosave, slot-1, slot-2, or slot-3)"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
_new_game_json() {
|
||||
local slot="$1"
|
||||
printf '{"slot":"%s","day":1,"trust":50,"questsCompleted":0,"quests":{},"flags":{},"inbox":[],"clock":{"shift":1,"day":1}}\n' \
|
||||
"$slot"
|
||||
}
|
||||
|
||||
save_list() {
|
||||
mkdir -p "$SC_SAVE_DIR"
|
||||
local active
|
||||
active="$(_save_active_slot)"
|
||||
printf ' %-14s %-10s %-10s %-10s\n' "Slot" "Day" "Trust" "Quests"
|
||||
printf ' %-14s %-10s %-10s %-10s\n' "──────────────" "──────────" "──────────" "──────────"
|
||||
local slot
|
||||
for slot in autosave slot-1 slot-2 slot-3; do
|
||||
local path
|
||||
path="$(_save_path "$slot")"
|
||||
if [ -f "$path" ]; then
|
||||
local day trust quests marker=""
|
||||
day="$( grep -o '"day":[0-9]*' "$path" 2>/dev/null | head -1 | cut -d: -f2 || echo '?')"
|
||||
trust="$( grep -o '"trust":[0-9]*' "$path" 2>/dev/null | head -1 | cut -d: -f2 || echo '?')"
|
||||
quests="$( grep -o '"questsCompleted":[0-9]*' "$path" 2>/dev/null | head -1 | cut -d: -f2 || echo '?')"
|
||||
[ "$slot" = "$active" ] && marker=" [active]"
|
||||
printf ' %-14s %-10s %-10s %-10s%s\n' "$slot" "Day $day" "T:$trust" "Q:$quests" "$marker"
|
||||
else
|
||||
printf ' %-14s %s\n' "$slot" "—empty—"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
save_switch() {
|
||||
local slot="$1"
|
||||
_save_valid_slot "$slot" || return 1
|
||||
[ -f "$(_save_path "$slot")" ] || { echo " ✗ No save in slot: $slot"; return 1; }
|
||||
printf '%s' "$slot" > "$SC_SAVE_DIR/.active"
|
||||
echo " ✓ Switched to $slot"
|
||||
}
|
||||
|
||||
save_new() {
|
||||
local slot="$1"
|
||||
_save_valid_slot "$slot" || return 1
|
||||
mkdir -p "$SC_SAVE_DIR"
|
||||
_new_game_json "$slot" > "$(_save_path "$slot")"
|
||||
echo " ✓ Created new save: $slot"
|
||||
}
|
||||
|
||||
save_reset() {
|
||||
local slot="${1:-$(_save_active_slot)}"
|
||||
_save_valid_slot "$slot" || return 1
|
||||
mkdir -p "$SC_SAVE_DIR"
|
||||
_new_game_json "$slot" > "$(_save_path "$slot")"
|
||||
echo " ✓ Reset $slot to new game state"
|
||||
}
|
||||
|
||||
save_export() {
|
||||
local slot="$1"
|
||||
local dest="$2"
|
||||
_save_valid_slot "$slot" || return 1
|
||||
[ -f "$(_save_path "$slot")" ] || { echo " ✗ No save in slot: $slot"; return 1; }
|
||||
[ -n "$dest" ] || { echo " ✗ No destination path given"; return 1; }
|
||||
cp "$(_save_path "$slot")" "$dest"
|
||||
echo " ✓ Exported $slot → $dest"
|
||||
}
|
||||
|
||||
save_import() {
|
||||
local src="$1"
|
||||
local slot="$2"
|
||||
_save_valid_slot "$slot" || return 1
|
||||
[ -f "$src" ] || { echo " ✗ File not found: $src"; return 1; }
|
||||
# Basic JSON sanity check
|
||||
grep -q '^{' "$src" 2>/dev/null || { echo " ✗ File does not look like a save: $src"; return 1; }
|
||||
mkdir -p "$SC_SAVE_DIR"
|
||||
cp "$src" "$(_save_path "$slot")"
|
||||
echo " ✓ Imported $src → $slot"
|
||||
}
|
||||
Executable
+124
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared UI helpers for Sysadmin Chronicles scripts.
|
||||
# Source this file; do not execute directly.
|
||||
|
||||
_SC_STEP_N=0
|
||||
|
||||
# Colors — disabled if stdout is not a terminal or NO_COLOR is set
|
||||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||||
_C_RESET='\033[0m'
|
||||
_C_BOLD='\033[1m'
|
||||
_C_GREEN='\033[0;32m'
|
||||
_C_YELLOW='\033[0;33m'
|
||||
_C_RED='\033[0;31m'
|
||||
_C_CYAN='\033[0;36m'
|
||||
_C_DIM='\033[2m'
|
||||
else
|
||||
_C_RESET='' _C_BOLD='' _C_GREEN='' _C_YELLOW='' _C_RED='' _C_CYAN='' _C_DIM=''
|
||||
fi
|
||||
|
||||
sc_header() {
|
||||
local title="${1:-Sysadmin Chronicles}"
|
||||
echo ""
|
||||
printf "${_C_CYAN}${_C_BOLD}"
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
printf "║ %-40s║\n" "$title"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
printf "${_C_RESET}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
sc_done_banner() {
|
||||
echo ""
|
||||
printf "${_C_GREEN}${_C_BOLD}"
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
printf "║ %-40s║\n" "SETUP COMPLETE!"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
printf "${_C_RESET}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
sc_section() {
|
||||
echo ""
|
||||
printf "${_C_BOLD}── %s ${_C_DIM}─────────────────────────────────${_C_RESET}\n" "$*"
|
||||
}
|
||||
|
||||
sc_step() {
|
||||
(( _SC_STEP_N++ )) || true
|
||||
echo ""
|
||||
printf "${_C_BOLD}── Step %d: %s${_C_RESET}\n" "$_SC_STEP_N" "$*"
|
||||
}
|
||||
|
||||
sc_ok() { printf " ${_C_GREEN}✓${_C_RESET} %s\n" "$*"; }
|
||||
sc_warn() { printf " ${_C_YELLOW}⚠${_C_RESET} %s\n" "$*"; }
|
||||
sc_err() { printf " ${_C_RED}✗${_C_RESET} %s\n" "$*" >&2; }
|
||||
sc_fail() { sc_err "$*"; exit 1; }
|
||||
sc_info() { printf " ${_C_DIM}→${_C_RESET} %s\n" "$*"; }
|
||||
|
||||
# Prompt — writes question to /dev/tty, returns answer on stdout
|
||||
sc_prompt() {
|
||||
local question="$1"
|
||||
local default="${2:-}"
|
||||
if [ -n "$default" ]; then
|
||||
printf " %s [%s] > " "$question" "$default" >/dev/tty
|
||||
else
|
||||
printf " %s > " "$question" >/dev/tty
|
||||
fi
|
||||
local answer
|
||||
read -r answer </dev/tty
|
||||
printf '%s' "${answer:-$default}"
|
||||
}
|
||||
|
||||
# Confirm — returns 0 for yes, 1 for no
|
||||
sc_confirm() {
|
||||
local question="$1"
|
||||
local default="${2:-Y}"
|
||||
local prompt
|
||||
if [ "${default^^}" = "Y" ]; then
|
||||
prompt="[Y/n]"
|
||||
else
|
||||
prompt="[y/N]"
|
||||
fi
|
||||
printf " %s %s " "$question" "$prompt" >/dev/tty
|
||||
local answer
|
||||
read -r answer </dev/tty
|
||||
answer="${answer:-$default}"
|
||||
case "${answer,,}" in
|
||||
y|yes) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
sc_progress() {
|
||||
local label="$1"
|
||||
local current="$2"
|
||||
local total="$3"
|
||||
printf " %-38s %d/%d\n" "$label" "$current" "$total"
|
||||
}
|
||||
|
||||
_SC_SPINNER_PID=""
|
||||
|
||||
sc_spinner() {
|
||||
local label="${1:-Working...}"
|
||||
printf " %s " "$label" >/dev/tty
|
||||
(
|
||||
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||
local i=0
|
||||
while true; do
|
||||
printf "\r %s %s " "$label" "${frames[$((i % ${#frames[@]}))]}" >/dev/tty
|
||||
sleep 0.12
|
||||
(( i++ )) || true
|
||||
done
|
||||
) &
|
||||
_SC_SPINNER_PID=$!
|
||||
disown "$_SC_SPINNER_PID" 2>/dev/null || true
|
||||
}
|
||||
|
||||
sc_spinner_stop() {
|
||||
if [ -n "${_SC_SPINNER_PID:-}" ]; then
|
||||
kill "$_SC_SPINNER_PID" 2>/dev/null || true
|
||||
wait "$_SC_SPINNER_PID" 2>/dev/null || true
|
||||
_SC_SPINNER_PID=""
|
||||
printf "\r%-60s\r" "" >/dev/tty
|
||||
fi
|
||||
}
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# VM operations for Sysadmin Chronicles.
|
||||
# Source this file; requires lib/libvirt.sh and PROJECT_ROOT set.
|
||||
|
||||
SC_VM_TOOLS="${SC_VM_TOOLS:-${PROJECT_ROOT:-}/tools/vm}"
|
||||
|
||||
_sc_validate_snapshot_name() {
|
||||
[[ "$1" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]*$ ]]
|
||||
}
|
||||
|
||||
_sc_protected_snapshot() {
|
||||
[[ "$1" == baseline.* ]] || [[ "$1" == checkpoint.* ]]
|
||||
}
|
||||
|
||||
vm_build() {
|
||||
local profile="$1"
|
||||
shift
|
||||
local dry_run=false force=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) dry_run=true ;;
|
||||
--force) force=true ;;
|
||||
esac
|
||||
done
|
||||
local script="$SC_VM_TOOLS/build-${profile}.sh"
|
||||
[ -f "$script" ] || { echo " ✗ No build script for profile: $profile"; return 1; }
|
||||
local args=()
|
||||
[ "$dry_run" = true ] && args+=(--dry-run)
|
||||
[ "$force" = true ] && args+=(--force)
|
||||
bash "$script" "${args[@]}"
|
||||
}
|
||||
|
||||
vm_rebuild() {
|
||||
local profile="$1"
|
||||
shift
|
||||
local dry_run=false
|
||||
for arg in "$@"; do [ "$arg" = "--dry-run" ] && dry_run=true; done
|
||||
|
||||
local domain="sc-${profile}"
|
||||
if domain_exists "$domain" && [ "$dry_run" = false ]; then
|
||||
virsh destroy "$domain" >/dev/null 2>&1 || true
|
||||
virsh undefine "$domain" --nvram --snapshots-metadata >/dev/null 2>&1 \
|
||||
|| virsh undefine "$domain" --snapshots-metadata >/dev/null 2>&1 \
|
||||
|| virsh undefine "$domain" >/dev/null 2>&1 || true
|
||||
fi
|
||||
local extra_args=()
|
||||
[ "$dry_run" = true ] && extra_args+=(--dry-run)
|
||||
vm_build "$profile" "${extra_args[@]}"
|
||||
}
|
||||
|
||||
vm_revert() {
|
||||
snapshot_revert "$1" "$2"
|
||||
}
|
||||
|
||||
vm_status() {
|
||||
local vm_id="$1"
|
||||
domain_exists "$vm_id" && domain_state "$vm_id" || printf 'missing'
|
||||
}
|
||||
|
||||
vm_start() {
|
||||
virsh start "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
vm_stop() {
|
||||
virsh shutdown "$1" >/dev/null 2>&1 || virsh destroy "$1" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
vm_snapshot_create() {
|
||||
local vm_id="$1"
|
||||
local name="$2"
|
||||
_sc_validate_snapshot_name "$name" \
|
||||
|| { echo " ✗ Invalid name (letters, numbers, hyphens only): $name"; return 1; }
|
||||
snapshot_create "$vm_id" "$name" "User snapshot — $(date '+%Y-%m-%d %H:%M')"
|
||||
}
|
||||
|
||||
vm_snapshot_list() {
|
||||
local vm_id="$1"
|
||||
virsh snapshot-list "$vm_id" 2>/dev/null || true
|
||||
}
|
||||
|
||||
vm_snapshot_revert() {
|
||||
local vm_id="$1"
|
||||
local name="$2"
|
||||
snapshot_exists "$vm_id" "$name" \
|
||||
|| { echo " ✗ Snapshot not found: $name"; return 1; }
|
||||
snapshot_revert "$vm_id" "$name"
|
||||
}
|
||||
|
||||
vm_snapshot_delete() {
|
||||
local vm_id="$1"
|
||||
local name="$2"
|
||||
if _sc_protected_snapshot "$name"; then
|
||||
echo " ✗ Cannot delete protected snapshot: $name"
|
||||
return 1
|
||||
fi
|
||||
snapshot_delete "$vm_id" "$name"
|
||||
}
|
||||
Reference in New Issue
Block a user