# Installer & Distribution Plan
> Status: Planning — not yet implemented.
> Covers: installer, uninstaller, VM rebuild, save management, modular script architecture.

---

## Goals

- Download zip from GitHub/Gitea, run `install.sh`, done.
- Friendly tone throughout — this is a game, not a server deployment.
- No jargon (libvirt, pool, domain, NAT) in any user-facing output.
- Power users can follow the Manual Install section in README instead.
- VM images live wherever the user puts the game (portable, large-drive friendly).
- Full uninstall with explicit choices about what gets removed.
- Users can rebuild individual VMs if something goes wrong.
- Save data is resettable; save slots available for experimenting.

---

## `start-game.sh` Fixes

The current launcher works but has two real bugs, several fragile assumptions, and
no user-friendly output. Fix this in the same pass as the rest of the scripts since
it will share `lib/ui.sh` and `lib/config.sh`.

### Bugs to fix

**Orphaned server process**
The script ends with `exec remote-viewer`, which replaces the shell. The `trap`
that was set to kill the server on EXIT disappears with the shell — so when the
player closes the SPICE window, the game server keeps running silently.

Fix: don't `exec`. Run `remote-viewer` normally, capture its PID, wait for it to
exit, then kill the server cleanly.

```bash
# instead of:
exec remote-viewer "$spice_uri"

# do:
remote-viewer "$spice_uri" &
VIEWER_PID=$!
trap 'kill "$SERVER_PID" "$VIEWER_PID" 2>/dev/null || true' EXIT INT TERM
wait "$VIEWER_PID"
```

**`sleep 1` server readiness check**
One second is a race. On a slow machine or if npm install just ran, the server
may not be up. On a fast machine it's wasted time.

Fix: poll in a tight loop with a timeout.

```bash
wait_for_server() {
    local port="$1" timeout=15 i=0
    while ! ss -tlnp | grep -q ":${port} " 2>/dev/null; do
        sleep 0.3
        ((i++))
        [ $i -ge $((timeout * 3)) ] && return 1
    done
}
```

### Fragile assumptions to fix

- **`lsof` for port check** — not universal. Replace with `ss -tlnp` (iproute2,
  present on all modern Linux).
- **No network check** — if the `sc-internal` libvirt network is inactive, the VM
  starts but has no network. The HUD loads but shows nothing. Check the network is
  active (and start it if not) before starting the VM.
- **No images-dir check** — once portable installs land, `SC_IMAGES_DIR` might be
  on an unmounted game drive. Check it exists before trying virsh ops.
- **Frontend build at launch** — `"Building frontend..."` at game launch is odd UX.
  Move this guard to install time. The launcher should only verify `dist/index.html`
  exists and fail clearly if it doesn't (don't silently trigger a build).

### UX improvements

- Source `lib/ui.sh` and `lib/config.sh` once they exist.
- Replace raw `echo "ERROR: ..."` with friendly messages. Examples:

| Current | Replacement |
|---|---|
| `ERROR: virsh is required.` | `Your system is missing the virtual machine tools.\nRun install.sh to set up the game.` |
| `ERROR: missing workstation domain: sc-workstation` | `Your game world hasn't been built yet.\nRun install.sh to finish setup.` |
| `ERROR: node is required. Install Node.js 18+.` | `Node.js is required but wasn't found.\nRun install.sh to set up the game.` |

- Show brief startup status so the player isn't staring at a blank terminal:

```
  Starting Sysadmin Chronicles...
  ✓ Game server running
  ✓ Workstation online
  Opening your desk...
```

- Add `--manage-saves` and `--reset-save` flags (forward to `tools/save/manage-saves.sh`).

### New flag: `--stop`

Since the server now outlives the viewer when fixed, add `start-game.sh --stop`
that kills any running game server process. Useful if something gets stuck.

### Summary of changes to `start-game.sh`

| Area | Change |
|---|---|
| Server shutdown | `exec` → normal run + `wait`, trap covers both server and viewer |
| Server readiness | `sleep 1` → poll loop with 15s timeout |
| Port check | `lsof` → `ss -tlnp` |
| Network check | Add: verify `sc-internal` active, start if not |
| Images dir check | Add: verify `SC_IMAGES_DIR` exists before virsh ops |
| Frontend build | Remove from launcher; fail clearly if dist missing |
| Error messages | Replace all with plain-English + fix instructions |
| Startup output | Add three-line status before opening SPICE |
| New flags | `--manage-saves`, `--reset-save`, `--stop` |

---

## Script Architecture

All user-facing scripts share a common library layer. No logic is duplicated.

```
tools/
  lib/
    ui.sh          # colored output, prompts, spinners, progress bars
    deps.sh        # distro detection, package name map, dep check/install
    libvirt.sh     # virsh wrappers: network, pool, domain, snapshot ops
    vm.sh          # build, rebuild, snapshot, revert per VM
    config.sh      # read/write install config (~/.config/sysadmin-chronicles/config)
    save.sh        # save slot management, reset helpers

install.sh         # project root — the entry point for new users
uninstall.sh       # project root — removal with options
start-game.sh      # project root — launcher (checks env, starts server, opens SPICE)

tools/
  setup/
    check-host.sh       # kept, improved UX, used internally by install.sh
    first-run-setup.sh  # kept as internal lib target or merged into install.sh
    seed-vms.sh         # kept as internal lib target, called by install.sh and rebuild
  vm/
    rebuild-vms.sh      # new: rebuild all or specific VMs
  save/
    manage-saves.sh     # new: list/switch/reset save slots
```

### `lib/ui.sh`
- `sc_step "label"` — numbered step header
- `sc_ok "msg"`, `sc_warn "msg"`, `sc_fail "msg"` — status lines
- `sc_prompt "question" "default"` — interactive prompt, returns answer
- `sc_confirm "question"` — yes/no, returns 0/1
- `sc_spinner "label"` / `sc_spinner_stop` — background spinner for long ops
- `sc_progress "label" current total` — simple fraction display

### `lib/deps.sh`
- `detect_distro` — sets `$SC_DISTRO` (arch, debian, ubuntu, fedora, opensuse)
- `map_packages` — translates canonical dep names to distro package names
- `check_deps` — returns list of missing deps
- `install_deps "pkg1 pkg2 ..."` — runs the right package manager with sudo, logs what was installed

### `lib/libvirt.sh`
- `ensure_network name xml_path`
- `ensure_pool name path`
- `pool_path name` — returns the pool's target directory
- `domain_exists name`, `domain_state name`
- `snapshot_exists domain name`
- `snapshot_create domain name description`
- `snapshot_revert domain name`
- `snapshot_delete domain name`

### `lib/vm.sh`
- `vm_build profile [--dry-run] [--force]` — wraps `build-vm.sh`
- `vm_rebuild profile [--dry-run]` — destroy + rebuild from cloud image
- `vm_revert vm_id snapshot_name` — revert to named snapshot
- `vm_status vm_id` — running / stopped / missing
- `vm_start vm_id`, `vm_stop vm_id`

### `lib/config.sh`
Config file lives at `~/.config/sysadmin-chronicles/config` (survives game dir moves).

Variables stored:
```bash
SC_GAME_DIR=/home/user/Games/sysadmin-chronicles
SC_IMAGES_DIR=/home/user/Games/sysadmin-chronicles/images
SC_LIBVIRT_URI=qemu:///system
SC_INSTALL_DATE=2026-04-27
SC_INSTALLED_DEPS="libvirt qemu-system-x86 ..."  # what we added, for the log
```

- `config_read` — sources the config file
- `config_write key value`
- `config_show` — pretty-prints current config

### `lib/save.sh`
- `save_list` — lists all save slots with name, date, trust score, quest progress
- `save_switch slot_name` — switch active save
- `save_new slot_name` — create a new empty save slot
- `save_reset [slot_name]` — wipe a slot back to new-game state
- `save_export slot_name path` — export save JSON for backup
- `save_import path slot_name` — import a save JSON

---

## Installer Design (`install.sh`)

### Phase 1 — Welcome

```
╔══════════════════════════════════════════╗
║       SYSADMIN CHRONICLES — SETUP       ║
╚══════════════════════════════════════════╝

Welcome! This installer will:
  • Install a few system tools (KVM, QEMU, libvirt)
  • Set up a private virtual network for the game
  • Build three virtual machines (~30 minutes, once only)

Where would you like to install the game?
  [default: ~/Games/sysadmin-chronicles]  >
```

### Phase 2 — System check (silent)

Internally calls `check_deps`. If all present, skip to Phase 4 silently.

### Phase 3 — Dependency install (only if needed)

```
Your system is missing the following tools:
  • KVM virtualization support (qemu-system-x86)
  • Virtual machine manager (libvirt, virt-install)
  • SPICE display viewer (virt-viewer)
  • Cloud image tools (cloud-image-utils, genisoimage)

Install them now? You'll be asked for your password.  [Y/n]
```

After install:
- Log installed packages to `~/.local/share/sysadmin-chronicles/install.log`
- Format: timestamp, package name, version, distro. Human-readable.
- Note at end: "This log is kept so you know exactly what was added. See it at: ..."

### Phase 4 — One-time network and storage setup

```
── Setting up game network ──────────────────
  ✓ Private game network created
  ✓ VM image storage configured at ~/Games/sysadmin-chronicles/images
  ✓ Game access keys generated
```

User never sees "libvirt", "storage pool", "sc-internal", "sc-images".

### Phase 5 — VM build

```
── Building your game world ─────────────────
  This happens once and takes about 30 minutes.
  You can leave this running in the background.

  Building workstation (1/3) ........... ✓  8m 14s
  Building web server   (2/3) ........... ✓  4m 02s
  Building build server (3/3) ........... ✓  5m 31s
  Setting up quest scenarios ........... ✓  1m 48s
```

### Phase 6 — Desktop entry

```
Create a desktop launcher so the game appears in your app menu?  [Y/n]
```

Creates `~/.local/share/applications/sysadmin-chronicles.desktop` if yes.

### Phase 7 — Done

```
╔══════════════════════════════════════════╗
║              SETUP COMPLETE!            ║
╚══════════════════════════════════════════╝

Start the game:
  bash ~/Games/sysadmin-chronicles/start-game.sh
  (or from your app menu if you created a launcher)

If you ever need to rebuild the virtual machines:
  bash ~/Games/sysadmin-chronicles/tools/vm/rebuild-vms.sh

Install log saved at:
  ~/.local/share/sysadmin-chronicles/install.log
```

---

## Uninstaller Design (`uninstall.sh`)

Improved from current: shows sizes, explains consequences, three-tier removal.

### Menu approach

```
╔══════════════════════════════════════════╗
║     SYSADMIN CHRONICLES — UNINSTALL     ║
╚══════════════════════════════════════════╝

What would you like to remove?

  1) Everything — full uninstall (recommended)
  2) Game world only — remove VMs, keep game files
  3) Save data only — reset to new game
  4) Custom — choose what to remove

  q) Cancel

>
```

### "Everything" breakdown (shows before confirming)

```
This will remove:

  Game virtual machines (3 VMs + all snapshots)   ~38 GB
  VM image files on disk                           ~38 GB  ← ask separately
  Game network and storage configuration           <1 MB
  Game access keys (~/.ssh/sc_host_key)            <1 KB
  Desktop launcher (if created)                    <1 KB

  System packages (libvirt, QEMU, etc.)            NOT removed
  ↑ These were installed by your package manager.
    See ~/.local/share/sysadmin-chronicles/install.log
    if you want to remove them manually.

Keep VM image files? If you ever reinstall, keeping them
saves the 30-minute rebuild.  [Y/n — default: keep]

Type REMOVE to confirm:  >
```

### What is never auto-removed

- System packages (libvirt, qemu, virt-viewer, etc.)
- Anything not prefixed with `sc-` in libvirt
- Any other libvirt VMs or networks not owned by this game

---

## VM Rebuild Tool (`tools/vm/rebuild-vms.sh`)

For when something goes wrong with a VM or the user wants a clean reset.

```
Usage:
  rebuild-vms.sh                  Rebuild all VMs from scratch
  rebuild-vms.sh --vm workstation Rebuild a single VM
  rebuild-vms.sh --revert         Revert all VMs to baseline snapshot (fast, ~30s)
  rebuild-vms.sh --revert --vm workstation

Menu (interactive):
  1) Revert all to last known good  (fast — restores baseline snapshot)
  2) Rebuild workstation            (~8 min — rebuilds from cloud image)
  3) Rebuild web server             (~4 min)
  4) Rebuild build server           (~5 min)
  5) Rebuild everything             (~20 min)
  q) Cancel
```

Key behavior:
- Always confirm before destroying a VM
- Show what quest progress will be affected
- Offer to back up save data before proceeding
- After rebuild, re-runs the appropriate quest-prep scripts and re-takes baseline snapshot

---

## User Snapshots

Players can take their own named snapshots of any VM — useful before attempting
something risky, or to bookmark a state they want to return to.

These are distinct from the game's automatic shift checkpoints and baseline
snapshots. User snapshots are never pruned automatically.

### Via `manage-saves.sh` (recommended)

The save management menu will include a **VM Snapshots** section:

```
VM Snapshots

  workstation (ares)
    1) before-ssh-experiment   2026-05-01 19:14
    2) checkpoint.shift-3      2026-05-01 22:00  [auto]
    3) baseline.day-one                          [protected]

  web server (hermes)
    1) my-nginx-fix            2026-05-02 11:30
    2) checkpoint.shift-3      2026-05-01 22:00  [auto]
    3) baseline.clean                            [protected]

  Actions: [t]ake snapshot  [r]evert  [d]elete  [q]uit
```

Taking a snapshot prompts for a name (letters, numbers, hyphens only).
Reverting shows a confirmation with the snapshot date.
Protected snapshots (baseline.*, checkpoint.*) cannot be deleted from this menu.

### Via `tools/vm/rebuild-vms.sh --snapshot`

For scripting or quick one-liners:

```bash
rebuild-vms.sh --snapshot --vm workstation --name before-risky-thing
rebuild-vms.sh --snapshot --all --name pre-shift-4
rebuild-vms.sh --revert  --vm workstation --name before-risky-thing
```

### Storage note

Each VM snapshot is an internal qcow2 differential — typically 100 MB–2 GB
depending on how much disk has changed since the baseline. The uninstaller shows
the total size of user snapshots separately so the user can decide whether to
keep them.

### `lib/vm.sh` additions needed

- `vm_snapshot_create vm_id name` — with name validation
- `vm_snapshot_list vm_id` — returns name, date, size, protection flag
- `vm_snapshot_revert vm_id name`
- `vm_snapshot_delete vm_id name` — refuses if name matches `baseline.*` or `checkpoint.*`

---

## Save Management

### Save file layout

```
~/.local/share/sysadmin-chronicles/
  saves/
    autosave.json          ← always-present auto save (current session)
    slot-1.json
    slot-2.json
    slot-3.json
  install.log
```

### Save slot semantics

Save slots store JSON state only:
- Trust score and history
- Quest and ticket state
- World flags
- Inbox
- In-world clock

**VM state is not per-slot.** The shift checkpoint snapshots (checkpoint.shift-N) are the VM save mechanism and are independent of JSON slots. This is a known limitation but keeps disk usage manageable.

When switching slots: if the VM state doesn't match the JSON slot's expected state, warn the user. They may need to revert VMs manually.

### `tools/save/manage-saves.sh`

```
Usage:
  manage-saves.sh                 Show save slot menu
  manage-saves.sh --reset         Reset current save to new game
  manage-saves.sh --reset slot-1  Reset a specific slot
  manage-saves.sh --list          List all slots

Interactive menu:
  Current save: autosave  (Day 3, Trust: 67, 4/8 quests)

  1) autosave   Day 3  Trust 67  Q4/8  [active]
  2) slot-1     Day 1  Trust 50  Q1/8
  3) slot-2     —empty—
  4) slot-3     —empty—

  Actions: [s]witch  [n]ew  [r]eset  [e]xport  [i]mport  [q]uit
```

### Reset save (standalone, accessible from start-game.sh)

The launcher `start-game.sh` should have an escape hatch:

```
start-game.sh --manage-saves     → opens save management menu
start-game.sh --reset-save       → confirms and resets to new game
```

---

## Launcher Improvements (`start-game.sh`)

Current issues to fix:
- Silently fails if images drive not mounted
- No check that the libvirt network is up before starting
- `sleep 1` to wait for server is fragile

Improvements:
- `config_read` to get `SC_IMAGES_DIR`, check it exists and is writable
- Check libvirt network is active, start it if not (with clear message)
- Poll server readiness on `/healthz` instead of sleeping
- Show a brief status before launching SPICE: "Starting your workstation..."
- On failure, show a plain-English error and the fix

---

## Portable Installation Notes

The `sc-images` libvirt pool target can be any path the host OS can write to. The installer configures it to `$SC_IMAGES_DIR` (inside the game dir by default). 

If the user puts the game on a game drive (`/mnt/gamesdrive/sysadmin-chronicles/`):
- `SC_IMAGES_DIR=/mnt/gamesdrive/sysadmin-chronicles/images`
- The libvirt pool points there
- All qcow2 files live on the game drive
- The launcher checks the drive is mounted before starting

If the drive is unmounted:
```
  ✗ Can't find your game world.
    The VM images are stored at /mnt/gamesdrive/sysadmin-chronicles/images
    but that location isn't available right now.

    Is your game drive plugged in and mounted?
    Once it's mounted, run start-game.sh again.
```

---

## Dependency Log Format

`~/.local/share/sysadmin-chronicles/install.log`

```
# Sysadmin Chronicles — Install Log
# Created: 2026-04-27 14:32:01
# Distro:  arch (6.19.12-arch1-1)
# Game dir: /home/aaron/Games/sysadmin-chronicles
# Images:   /home/aaron/Games/sysadmin-chronicles/images

[INSTALLED] libvirt                  12.2.0   via pacman
[INSTALLED] qemu-system-x86         11.0.0   via pacman
[INSTALLED] qemu-hw-display-qxl     11.0.0   via pacman
[INSTALLED] qemu-hw-display-virtio-gpu  11.0.0  via pacman
[INSTALLED] qemu-ui-spice-core      11.0.0   via pacman
[INSTALLED] qemu-chardev-spice      11.0.0   via pacman
[INSTALLED] qemu-audio-spice        11.0.0   via pacman
[INSTALLED] virt-install            5.1.0    via pacman
[INSTALLED] virt-viewer             11.0     via pacman
[INSTALLED] cloud-image-utils       0.33     via pacman
[INSTALLED] cdrtools                3.02a09  via pacman
[INSTALLED] libisoburn              1.5.8    via pacman
[SKIPPED]   nodejs                           already installed

# To remove manually:
# sudo pacman -Rns libvirt qemu-system-x86 qemu-hw-display-qxl ...
```

---

## File Layout After Install

```
~/Games/sysadmin-chronicles/     ← SC_GAME_DIR
  install.sh
  uninstall.sh
  start-game.sh
  content/
  server/
  frontend/
  docs/
  tools/
    lib/
      ui.sh
      deps.sh
      libvirt.sh
      vm.sh
      config.sh
      save.sh
    setup/
      check-host.sh
      first-run-setup.sh
      seed-vms.sh
    vm/
      rebuild-vms.sh
      build-vm.sh
      ...
    save/
      manage-saves.sh

  images/                        ← SC_IMAGES_DIR (libvirt pool points here)
    sc-workstation.qcow2         (~20 GB)
    sc-web-server.qcow2          (~8 GB)
    sc-build-machine.qcow2       (~10 GB)

~/.config/sysadmin-chronicles/config      ← install config (survives game dir moves)
~/.local/share/sysadmin-chronicles/
  saves/
    autosave.json
    slot-1.json ...
  install.log
```

---

## Implementation Order

1. `tools/lib/ui.sh` — all other scripts depend on this
2. `tools/lib/config.sh` — needed by installer and launcher
3. `tools/lib/deps.sh` — needed by installer
4. `tools/lib/libvirt.sh` — needed by installer and rebuild tool
5. `tools/lib/vm.sh` — needed by installer and rebuild tool
6. `tools/lib/save.sh` — needed by save manager
7. `install.sh` — assembles libs 1–5
8. `tools/vm/rebuild-vms.sh` — assembles libs 1, 3, 4
9. `tools/save/manage-saves.sh` — assembles libs 1, 2, 6
10. `uninstall.sh` — assembles libs 1, 2, 4
11. `start-game.sh` (improved) — assembles libs 1, 2
12. Update `check-host.sh` UX
13. README — manual install section, quick start

---

## README Structure

```markdown
## Quick Install

curl -fsSL .../install.sh | bash
# or
bash install.sh   # from downloaded zip

## Manual Install

<details>
<summary>For users who want full control or are troubleshooting</summary>
...per-distro dep tables, step-by-step...
</details>
```
