openvibers/openvibe.games Commits
deploy/scripts/deploy.sh: deploys run through ovhost deploy games (OpenVibe.Host strategy pnpm-build; roadmap WS-N task 11). ovhost does the production procedure (pull, pnpm install --frozen-lockfile, pnpm build, restart openvibe-games) with git as the checkout owner instead of sudo git, restores the tracked dist-types/ a previous build rewrote before the pull, checks every workspace package's dependencies and that better-sqlite3 loads under the host's Node before the restart, polls /api/ready, and on failure restores, reinstalls and rebuilds (the build writes the served client) and restarts again. Players online are reported and reconnect (drain policy report); --wait-idle holds the restart until nobody plays. The wrapper passes --wait-idle, --restart, --force, --rollback and DRY_RUN=1 (ovhost plan games); it asks ovhost capabilities games first and falls back to deploy/scripts/deploy-legacy.sh, the procedure as it was run by hand (sudo git -c safe.directory pull, pnpm install --frozen-lockfile, pnpm build, sudo systemctl restart openvibe-games, then a readiness wait and the best-effort release notification), when ovhost is missing, too old or the host inventory does not deploy games with pnpm-build (OVHOST_LEGACY=1 forces it). apps/server/scripts/deployWrapper.test.ts. README.
Changes
@@@ -95,6 +95,19 @@ has ticked within the last 5 s, otherwise 503 naming the failed check (`status`Deployed at https://openvibe.games (nginx TLS termination → server on :8000,systemd unit `openvibe-games.service`; play.openvibe.games Host-routes to the same process).++Deploy on the host with `sudo /opt/openvibe.games/deploy/scripts/deploy.sh`, which runs++`ovhost deploy games` (OpenVibe.Host, strategy `pnpm-build`; roadmap WS-N task 11): a fast-forward pull as++the checkout owner (the tracked `dist-types/` a previous build rewrote is restored first),++`pnpm install --frozen-lockfile`, every workspace package's dependencies checked, `pnpm build`,++better-sqlite3 loaded under the host's Node, the restart, `/api/ready`, and on failure the checkout++restored, reinstalled and rebuilt and the server restarted again. Players online are reported and++reconnect; `--wait-idle` holds the restart until nobody plays, `--rollback` runs `ovhost rollback games`,++`DRY_RUN=1` prints `ovhost plan games`. Do not pull by hand first (ovhost would find nothing new; pass++`--restart` if you did). When ovhost is missing, too old or does not deploy Games with that strategy, the++wrapper runs `deploy/scripts/deploy-legacy.sh`: the procedure as it was run by hand++(`sudo git -c safe.directory=/opt/openvibe.games pull`, `pnpm install --frozen-lockfile`, `pnpm build`,++`sudo systemctl restart openvibe-games`).++## Platform integration (OpenVibe network)Games integrates with the rest of OpenVibe at its service boundary onlynnew file mode 100644@@@ -0,0 +1,105 @@++import { spawnSync } from 'node:child_process'++import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'++import { tmpdir } from 'node:os'++import { dirname, join } from 'node:path'++import { fileURLToPath } from 'node:url'++import { afterAll, describe, expect, it } from 'vitest'++++/**++ * deploy/scripts/deploy.sh is a thin wrapper around `ovhost deploy games` (OpenVibe.Host, strategy++ * pnpm-build; roadmap WS-N task 11), with deploy-legacy.sh (the production procedure as it was run by++ * hand) as its fallback. A fake ovhost records what the wrapper asks for; a fake legacy script records++ * the fallback.++ */++++const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')++const WRAPPER = join(ROOT, 'deploy', 'scripts', 'deploy.sh')++const tmp = mkdtempSync(join(tmpdir(), 'games-deploy-wrapper-'))++const log = join(tmp, 'calls.log')++const ovhost = join(tmp, 'ovhost')++writeFileSync(++ ovhost,++ `#!/usr/bin/env bash++if [ "$1" = capabilities ]; then++ [ -n "$FAKE_OLD" ] && exit 1++ printf '%b\\n' "\${FAKE_CAPS:-ovhost=0.3.0\\ndeploy-api=1\\nservice=games\\nstrategy=pnpm-build\\nmanaged=yes\\nlayout=git}"++ exit 0++fi++echo "ovhost $*" >> "${log}"++exit "\${FAKE_EXIT:-0}"++`,++)++chmodSync(ovhost, 0o755)++const legacy = join(tmp, 'legacy.sh')++writeFileSync(legacy, `#!/usr/bin/env bash\necho "legacy" >> "${log}"\n`)++chmodSync(legacy, 0o755)++++interface Run {++ code: number | null++ out: string++ calls: string[]++}++++function run(args: string[] = [], env: Record<string, string> = {}): Run {++ rmSync(log, { force: true })++ const r = spawnSync('bash', [WRAPPER, ...args], {++ env: {++ PATH: process.env.PATH ?? '',++ OVHOST: ovhost,++ OVHOST_SUDO: '',++ DEPLOY_LEGACY: legacy,++ ...env,++ },++ encoding: 'utf8',++ })++ let calls: string[] = []++ try {++ calls = readFileSync(log, 'utf8').trim().split('\n').filter(Boolean)++ } catch {++ calls = []++ }++ return { code: r.status, out: r.stdout + r.stderr, calls }++}++++afterAll(() => rmSync(tmp, { recursive: true, force: true }))++++describe('deploy/scripts/deploy.sh', () => {++ it('maps its flags onto ovhost deploy|rollback|plan games', () => {++ expect(run().calls).toEqual(['ovhost deploy games'])++ expect(run(['--wait-idle']).calls).toEqual(['ovhost deploy games --wait-idle'])++ expect(run(['--restart']).calls).toEqual(['ovhost deploy games --restart'])++ expect(run(['--rollback']).calls).toEqual(['ovhost rollback games'])++ expect(run([], { DRY_RUN: '1' }).calls).toEqual(['ovhost plan games'])++ expect(run([], { FAKE_EXIT: '3' }).code).toBe(3)++ expect(run(['--nope']).code).toBe(1)++ })++++ it('falls back to deploy-legacy.sh when ovhost is missing, too old or not deploying games with pnpm-build', () => {++ let r = run([], { FAKE_OLD: '1' })++ expect(r.calls).toEqual(['legacy'])++ expect(r.out).toMatch(/too old/)++ r = run([], { FAKE_CAPS: 'deploy-api=1\\nstrategy=none\\nmanaged=no' })++ expect(r.calls).toEqual(['legacy'])++ expect(r.out).toMatch(/does not deploy games with strategy pnpm-build \(none\)/)++ expect(run([], { OVHOST: join(tmp, 'missing') }).calls).toEqual(['legacy'])++ // The legacy procedure cannot roll back, wait for idle or plan: nothing runs rather than something else.++ for (const args of [['--rollback'], ['--wait-idle']]) {++ r = run(args, { OVHOST_LEGACY: '1' })++ expect(r.code).toBe(1)++ expect(r.calls).toEqual([])++ }++ })++++ it('keeps the production procedure as the fallback', () => {++ const text = readFileSync(join(ROOT, 'deploy', 'scripts', 'deploy-legacy.sh'), 'utf8')++ for (const step of [++ 'sudo git -c safe.directory="$REPO" pull',++ 'pnpm install --frozen-lockfile',++ 'pnpm build',++ 'sudo systemctl restart openvibe-games',++ ]) {++ expect(text).toContain(step)++ }++ expect(readFileSync(WRAPPER, 'utf8')).toMatch(/^set -euo pipefail$/m)++ })++})nnew file mode 100755@@@ -0,0 +1,35 @@++#!/usr/bin/env bash++# OpenVibe.Games — the production deploy as it was done by hand before `ovhost deploy games`, kept as the++# fallback of deploy/scripts/deploy.sh (roadmap WS-N task 11). The same commands, in the same order:++#++# sudo git -c safe.directory=/opt/openvibe.games pull++# pnpm install --frozen-lockfile++# pnpm build++# sudo systemctl restart openvibe-games++#++# plus a readiness wait and the best-effort release notification the other repositories' scripts send. It++# has no protected-session check, no dependency check and no rollback: ovhost has those.++set -euo pipefail++REPO="${REPO:-/opt/openvibe.games}"++READY_URL="${READY_URL:-http://127.0.0.1:8000/api/ready}"++cd "$REPO"++sudo git -c safe.directory="$REPO" pull++pnpm install --frozen-lockfile++pnpm build++sudo systemctl restart openvibe-games++for _ in $(seq 1 60); do++ if curl -sf --max-time 2 "$READY_URL" >/dev/null; then echo "[games] ready ($(git -c safe.directory="$REPO" rev-parse --short=12 HEAD))"; break; fi++ sleep 1++done++curl -sf --max-time 2 "$READY_URL" >/dev/null || { echo "[games] ✗ not ready after 60 s: journalctl -u openvibe-games -n 50" >&2; exit 3; }++# Release notification (WS-P task 9): best effort, skipped without an ovhost that has `announce`.++OVHOST_BIN=$(command -v "${OVHOST:-ovhost}" 2>/dev/null || true)++if [ -n "$OVHOST_BIN" ]; then++ case "$("$OVHOST_BIN" --help 2>/dev/null || true)" in++ *"announce <service>"*)++ SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo -n"++ timeout 20 $SUDO "$OVHOST_BIN" announce games 2>&1 || echo "release notification not sent (the deploy stands)" ;;++ *) echo "release notification skipped: this ovhost has no announce" ;;++ esac++fi++exit 0nnew file mode 100755@@@ -0,0 +1,69 @@++#!/usr/bin/env bash++# OpenVibe.Games — deploy: a thin wrapper around `ovhost deploy games` (OpenVibe.Host, strategy pnpm-build;++# roadmap WS-N task 11; OpenVibe.Host docs/deploy-strategies.md). Run on the host:++#++# sudo /opt/openvibe.games/deploy/scripts/deploy.sh ovhost deploy games++# sudo /opt/openvibe.games/deploy/scripts/deploy.sh --wait-idle … --wait-idle (hold until nobody plays)++# sudo /opt/openvibe.games/deploy/scripts/deploy.sh --restart … --restart (restart even with nothing new)++# sudo /opt/openvibe.games/deploy/scripts/deploy.sh --rollback ovhost rollback games++# DRY_RUN=1 /opt/openvibe.games/deploy/scripts/deploy.sh ovhost plan games++#++# ovhost does the production procedure (pull, pnpm install --frozen-lockfile, pnpm build, restart++# openvibe-games) with git as the checkout owner instead of sudo git; the tracked dist-types/ a previous build++# rewrote restored from git before the pull; every workspace package's dependencies checked and better-sqlite3++# loaded under this Node before the restart; /api/ready polled; on failure the checkout restored, reinstalled++# and rebuilt (the build writes the served client) and the unit restarted again (exit 3). Players online are++# reported and reconnect (drain policy report); --wait-idle holds the restart until nobody plays. Do not pull++# by hand first: ovhost would find nothing new (pass --restart if you did).++#++# Fallback: deploy-legacy.sh (the procedure as it was run by hand) when ovhost is missing or too old (no++# `capabilities`, deploy-api < 1), or the host inventory does not deploy games with strategy pnpm-build;++# OVHOST_LEGACY=1 forces it. It takes no flags: --rollback, --wait-idle and DRY_RUN=1 are refused there.++set -euo pipefail++++SERVICE=games++STRATEGY=pnpm-build++HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)++LEGACY="${DEPLOY_LEGACY:-$HERE/deploy-legacy.sh}"++OVHOST="${OVHOST:-/usr/local/bin/ovhost}"++if [ "${OVHOST_SUDO-auto}" = auto ]; then if [ "$(id -u)" -eq 0 ]; then SUDO=(); else SUDO=(sudo); fi; elif [ -n "${OVHOST_SUDO}" ]; then SUDO=("$OVHOST_SUDO"); else SUDO=(); fi++++CMD=deploy++FLAGS=()++while [ "$#" -gt 0 ]; do++ case "$1" in++ --wait-idle|--restart|--force) FLAGS+=("$1"); shift ;;++ --rollback) CMD=rollback; shift ;;++ --) shift; break ;;++ *) echo "Usage: $0 [--wait-idle] [--restart] [--force] [--rollback] (DRY_RUN=1 for the plan)"; exit 1 ;;++ esac++done++++legacy() {++ echo "[games] $1 — running deploy-legacy.sh (the previous deploy procedure) instead"++ if [ "$CMD" = rollback ] || [ "${DRY_RUN:-0}" = 1 ] || [[ " ${FLAGS[*]:-} " == *" --wait-idle "* ]]; then++ echo "[games] ✗ deploy-legacy.sh has no --rollback, --wait-idle or DRY_RUN; nothing was done" >&2++ exit 1++ fi++ exec bash "$LEGACY"++}++++REASON=""++probe() {++ if [ "${OVHOST_LEGACY:-0}" = 1 ]; then REASON="OVHOST_LEGACY=1"; return 1; fi++ if ! command -v "$OVHOST" >/dev/null 2>&1; then REASON="ovhost not found ($OVHOST)"; return 1; fi++ local caps api++ if ! caps=$("${SUDO[@]}" "$OVHOST" capabilities "$SERVICE" 2>/dev/null); then REASON="this ovhost has no 'capabilities' (too old) or no inventory entry for $SERVICE"; return 1; fi++ api=$(printf '%s\n' "$caps" | sed -n 's/^deploy-api=//p')++ case "$api" in ''|*[!0-9]*) REASON="this ovhost reports no deploy-api (too old)"; return 1 ;; esac++ if [ "$api" -lt 1 ]; then REASON="this ovhost's deploy-api is $api, 1 is needed"; return 1; fi++ if ! printf '%s\n' "$caps" | grep -qx "strategy=$STRATEGY"; then REASON="the host inventory does not deploy $SERVICE with strategy $STRATEGY ($(printf '%s\n' "$caps" | sed -n 's/^strategy=//p'))"; return 1; fi++ if ! printf '%s\n' "$caps" | grep -qx "managed=yes"; then REASON="ovhost does not manage $SERVICE"; return 1; fi++ return 0++}++++probe || legacy "$REASON"++++if [ "${DRY_RUN:-0}" = 1 ]; then exec "${SUDO[@]}" "$OVHOST" plan "$SERVICE"; fi++echo "[games] ovhost $CMD $SERVICE ${FLAGS[*]:-}"++exec "${SUDO[@]}" "$OVHOST" "$CMD" "$SERVICE" "${FLAGS[@]}"
@@ -95,6 +95,19 @@ has ticked within the last 5 s, otherwise 503 naming the failed check (`status`
@@ -95,6 +95,19 @@ has ticked within the last 5 s, otherwise 503 naming the failed check (`status`
Deployed at https://openvibe.games (nginx TLS termination → server on :8000,
Deployed at https://openvibe.games (nginx TLS termination → server on :8000,
systemd unit `openvibe-games.service`; play.openvibe.games Host-routes to the same process).
systemd unit `openvibe-games.service`; play.openvibe.games Host-routes to the same process).
+Deploy on the host with `sudo /opt/openvibe.games/deploy/scripts/deploy.sh`, which runs
+`ovhost deploy games` (OpenVibe.Host, strategy `pnpm-build`; roadmap WS-N task 11): a fast-forward pull as
+the checkout owner (the tracked `dist-types/` a previous build rewrote is restored first),
+`pnpm install --frozen-lockfile`, every workspace package's dependencies checked, `pnpm build`,
+better-sqlite3 loaded under the host's Node, the restart, `/api/ready`, and on failure the checkout
+restored, reinstalled and rebuilt and the server restarted again. Players online are reported and
+reconnect; `--wait-idle` holds the restart until nobody plays, `--rollback` runs `ovhost rollback games`,
+`DRY_RUN=1` prints `ovhost plan games`. Do not pull by hand first (ovhost would find nothing new; pass
+`--restart` if you did). When ovhost is missing, too old or does not deploy Games with that strategy, the
+wrapper runs `deploy/scripts/deploy-legacy.sh`: the procedure as it was run by hand
+(`sudo git -c safe.directory=/opt/openvibe.games pull`, `pnpm install --frozen-lockfile`, `pnpm build`,
+`sudo systemctl restart openvibe-games`).
+
## Platform integration (OpenVibe network)
## Platform integration (OpenVibe network)
Games integrates with the rest of OpenVibe at its service boundary only
Games integrates with the rest of OpenVibe at its service boundary only
new file mode 100644
new file mode 100644
@@ -0,0 +1,105 @@
@@ -0,0 +1,105 @@
+import { spawnSync } from 'node:child_process'
+import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { afterAll, describe, expect, it } from 'vitest'
+
+/**
+ * deploy/scripts/deploy.sh is a thin wrapper around `ovhost deploy games` (OpenVibe.Host, strategy
+ * pnpm-build; roadmap WS-N task 11), with deploy-legacy.sh (the production procedure as it was run by
+ * hand) as its fallback. A fake ovhost records what the wrapper asks for; a fake legacy script records
+ * the fallback.
+ */
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
+const WRAPPER = join(ROOT, 'deploy', 'scripts', 'deploy.sh')
+const tmp = mkdtempSync(join(tmpdir(), 'games-deploy-wrapper-'))
+const log = join(tmp, 'calls.log')
+const ovhost = join(tmp, 'ovhost')
+writeFileSync(
+ ovhost,
+ `#!/usr/bin/env bash
+if [ "$1" = capabilities ]; then
+ [ -n "$FAKE_OLD" ] && exit 1
+ printf '%b\\n' "\${FAKE_CAPS:-ovhost=0.3.0\\ndeploy-api=1\\nservice=games\\nstrategy=pnpm-build\\nmanaged=yes\\nlayout=git}"
+ exit 0
+fi
+echo "ovhost $*" >> "${log}"
+exit "\${FAKE_EXIT:-0}"
+`,
+)
+chmodSync(ovhost, 0o755)
+const legacy = join(tmp, 'legacy.sh')
+writeFileSync(legacy, `#!/usr/bin/env bash\necho "legacy" >> "${log}"\n`)
+chmodSync(legacy, 0o755)
+
+interface Run {
+ code: number | null
+ out: string
+ calls: string[]
+}
+
+function run(args: string[] = [], env: Record<string, string> = {}): Run {
+ rmSync(log, { force: true })
+ const r = spawnSync('bash', [WRAPPER, ...args], {
+ env: {
+ PATH: process.env.PATH ?? '',
+ OVHOST: ovhost,
+ OVHOST_SUDO: '',
+ DEPLOY_LEGACY: legacy,
+ ...env,
+ },
+ encoding: 'utf8',
+ })
+ let calls: string[] = []
+ try {
+ calls = readFileSync(log, 'utf8').trim().split('\n').filter(Boolean)
+ } catch {
+ calls = []
+ }
+ return { code: r.status, out: r.stdout + r.stderr, calls }
+}
+
+afterAll(() => rmSync(tmp, { recursive: true, force: true }))
+
+describe('deploy/scripts/deploy.sh', () => {
+ it('maps its flags onto ovhost deploy|rollback|plan games', () => {
+ expect(run().calls).toEqual(['ovhost deploy games'])
+ expect(run(['--wait-idle']).calls).toEqual(['ovhost deploy games --wait-idle'])
+ expect(run(['--restart']).calls).toEqual(['ovhost deploy games --restart'])
+ expect(run(['--rollback']).calls).toEqual(['ovhost rollback games'])
+ expect(run([], { DRY_RUN: '1' }).calls).toEqual(['ovhost plan games'])
+ expect(run([], { FAKE_EXIT: '3' }).code).toBe(3)
+ expect(run(['--nope']).code).toBe(1)
+ })
+
+ it('falls back to deploy-legacy.sh when ovhost is missing, too old or not deploying games with pnpm-build', () => {
+ let r = run([], { FAKE_OLD: '1' })
+ expect(r.calls).toEqual(['legacy'])
+ expect(r.out).toMatch(/too old/)
+ r = run([], { FAKE_CAPS: 'deploy-api=1\\nstrategy=none\\nmanaged=no' })
+ expect(r.calls).toEqual(['legacy'])
+ expect(r.out).toMatch(/does not deploy games with strategy pnpm-build \(none\)/)
+ expect(run([], { OVHOST: join(tmp, 'missing') }).calls).toEqual(['legacy'])
+ // The legacy procedure cannot roll back, wait for idle or plan: nothing runs rather than something else.
+ for (const args of [['--rollback'], ['--wait-idle']]) {
+ r = run(args, { OVHOST_LEGACY: '1' })
+ expect(r.code).toBe(1)
+ expect(r.calls).toEqual([])
+ }
+ })
+
+ it('keeps the production procedure as the fallback', () => {
+ const text = readFileSync(join(ROOT, 'deploy', 'scripts', 'deploy-legacy.sh'), 'utf8')
+ for (const step of [
+ 'sudo git -c safe.directory="$REPO" pull',
+ 'pnpm install --frozen-lockfile',
+ 'pnpm build',
+ 'sudo systemctl restart openvibe-games',
+ ]) {
+ expect(text).toContain(step)
+ }
+ expect(readFileSync(WRAPPER, 'utf8')).toMatch(/^set -euo pipefail$/m)
+ })
+})
new file mode 100755
new file mode 100755
@@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# OpenVibe.Games — the production deploy as it was done by hand before `ovhost deploy games`, kept as the
+# fallback of deploy/scripts/deploy.sh (roadmap WS-N task 11). The same commands, in the same order:
+#
+# sudo git -c safe.directory=/opt/openvibe.games pull
+# pnpm install --frozen-lockfile
+# pnpm build
+# sudo systemctl restart openvibe-games
+#
+# plus a readiness wait and the best-effort release notification the other repositories' scripts send. It
+# has no protected-session check, no dependency check and no rollback: ovhost has those.
+set -euo pipefail
+REPO="${REPO:-/opt/openvibe.games}"
+READY_URL="${READY_URL:-http://127.0.0.1:8000/api/ready}"
+cd "$REPO"
+sudo git -c safe.directory="$REPO" pull
+pnpm install --frozen-lockfile
+pnpm build
+sudo systemctl restart openvibe-games
+for _ in $(seq 1 60); do
+ if curl -sf --max-time 2 "$READY_URL" >/dev/null; then echo "[games] ready ($(git -c safe.directory="$REPO" rev-parse --short=12 HEAD))"; break; fi
+ sleep 1
+done
+curl -sf --max-time 2 "$READY_URL" >/dev/null || { echo "[games] ✗ not ready after 60 s: journalctl -u openvibe-games -n 50" >&2; exit 3; }
+# Release notification (WS-P task 9): best effort, skipped without an ovhost that has `announce`.
+OVHOST_BIN=$(command -v "${OVHOST:-ovhost}" 2>/dev/null || true)
+if [ -n "$OVHOST_BIN" ]; then
+ case "$("$OVHOST_BIN" --help 2>/dev/null || true)" in
+ *"announce <service>"*)
+ SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo -n"
+ timeout 20 $SUDO "$OVHOST_BIN" announce games 2>&1 || echo "release notification not sent (the deploy stands)" ;;
+ *) echo "release notification skipped: this ovhost has no announce" ;;
+ esac
+fi
+exit 0
new file mode 100755
new file mode 100755
@@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+# OpenVibe.Games — deploy: a thin wrapper around `ovhost deploy games` (OpenVibe.Host, strategy pnpm-build;
+# roadmap WS-N task 11; OpenVibe.Host docs/deploy-strategies.md). Run on the host:
+#
+# sudo /opt/openvibe.games/deploy/scripts/deploy.sh ovhost deploy games
+# sudo /opt/openvibe.games/deploy/scripts/deploy.sh --wait-idle … --wait-idle (hold until nobody plays)
+# sudo /opt/openvibe.games/deploy/scripts/deploy.sh --restart … --restart (restart even with nothing new)
+# sudo /opt/openvibe.games/deploy/scripts/deploy.sh --rollback ovhost rollback games
+# DRY_RUN=1 /opt/openvibe.games/deploy/scripts/deploy.sh ovhost plan games
+#
+# ovhost does the production procedure (pull, pnpm install --frozen-lockfile, pnpm build, restart
+# openvibe-games) with git as the checkout owner instead of sudo git; the tracked dist-types/ a previous build
+# rewrote restored from git before the pull; every workspace package's dependencies checked and better-sqlite3
+# loaded under this Node before the restart; /api/ready polled; on failure the checkout restored, reinstalled
+# and rebuilt (the build writes the served client) and the unit restarted again (exit 3). Players online are
+# reported and reconnect (drain policy report); --wait-idle holds the restart until nobody plays. Do not pull
+# by hand first: ovhost would find nothing new (pass --restart if you did).
+#
+# Fallback: deploy-legacy.sh (the procedure as it was run by hand) when ovhost is missing or too old (no
+# `capabilities`, deploy-api < 1), or the host inventory does not deploy games with strategy pnpm-build;
+# OVHOST_LEGACY=1 forces it. It takes no flags: --rollback, --wait-idle and DRY_RUN=1 are refused there.
+set -euo pipefail
+
+SERVICE=games
+STRATEGY=pnpm-build
+HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+LEGACY="${DEPLOY_LEGACY:-$HERE/deploy-legacy.sh}"
+OVHOST="${OVHOST:-/usr/local/bin/ovhost}"
+if [ "${OVHOST_SUDO-auto}" = auto ]; then if [ "$(id -u)" -eq 0 ]; then SUDO=(); else SUDO=(sudo); fi; elif [ -n "${OVHOST_SUDO}" ]; then SUDO=("$OVHOST_SUDO"); else SUDO=(); fi
+
+CMD=deploy
+FLAGS=()
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --wait-idle|--restart|--force) FLAGS+=("$1"); shift ;;
+ --rollback) CMD=rollback; shift ;;
+ --) shift; break ;;
+ *) echo "Usage: $0 [--wait-idle] [--restart] [--force] [--rollback] (DRY_RUN=1 for the plan)"; exit 1 ;;
+ esac
+done
+
+legacy() {
+ echo "[games] $1 — running deploy-legacy.sh (the previous deploy procedure) instead"
+ if [ "$CMD" = rollback ] || [ "${DRY_RUN:-0}" = 1 ] || [[ " ${FLAGS[*]:-} " == *" --wait-idle "* ]]; then
+ echo "[games] ✗ deploy-legacy.sh has no --rollback, --wait-idle or DRY_RUN; nothing was done" >&2
+ exit 1
+ fi
+ exec bash "$LEGACY"
+}
+
+REASON=""
+probe() {
+ if [ "${OVHOST_LEGACY:-0}" = 1 ]; then REASON="OVHOST_LEGACY=1"; return 1; fi
+ if ! command -v "$OVHOST" >/dev/null 2>&1; then REASON="ovhost not found ($OVHOST)"; return 1; fi
+ local caps api
+ if ! caps=$("${SUDO[@]}" "$OVHOST" capabilities "$SERVICE" 2>/dev/null); then REASON="this ovhost has no 'capabilities' (too old) or no inventory entry for $SERVICE"; return 1; fi
+ api=$(printf '%s\n' "$caps" | sed -n 's/^deploy-api=//p')
+ case "$api" in ''|*[!0-9]*) REASON="this ovhost reports no deploy-api (too old)"; return 1 ;; esac
+ if [ "$api" -lt 1 ]; then REASON="this ovhost's deploy-api is $api, 1 is needed"; return 1; fi
+ if ! printf '%s\n' "$caps" | grep -qx "strategy=$STRATEGY"; then REASON="the host inventory does not deploy $SERVICE with strategy $STRATEGY ($(printf '%s\n' "$caps" | sed -n 's/^strategy=//p'))"; return 1; fi
+ if ! printf '%s\n' "$caps" | grep -qx "managed=yes"; then REASON="ovhost does not manage $SERVICE"; return 1; fi
+ return 0
+}
+
+probe || legacy "$REASON"
+
+if [ "${DRY_RUN:-0}" = 1 ]; then exec "${SUDO[@]}" "$OVHOST" plan "$SERVICE"; fi
+echo "[games] ovhost $CMD $SERVICE ${FLAGS[*]:-}"
+exec "${SUDO[@]}" "$OVHOST" "$CMD" "$SERVICE" "${FLAGS[@]}"