openvibers/openvibe.games Commits
Games: the legacy deploy fallback is deleted; deploy.sh only runs ovhost (#26)
Changes
@@@ -143,10 +143,7 @@ the restart (the SDK migrator applies any new migration as the owner before servrestored, reinstalled and rebuilt and the server restarted again. Players online are reported andreconnect; `--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`).++`--restart` if you did). The wrapper exits with an error if ovhost is missing.Rollback: `sudo deploy/scripts/deploy.sh --rollback` (`ovhost rollback games`), which rebuilds the previouscommit. Migrations are additive and applied at boot, so an older build starts on the newer schema; the SDK@@@ -1,5 +1,5 @@import { spawnSync } from 'node:child_process'--import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'++import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'import { tmpdir } from 'node:os'import { dirname, join } from 'node:path'import { fileURLToPath } from 'node:url'@@@ -7,9 +7,7 @@ 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.++ * pnpm-build; roadmap WS-N task 11). A fake ovhost records what the wrapper asks for.*/const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')@@@ -20,20 +18,11 @@ 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--fiecho "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 | nullout: string@@@ -47,7 +36,6 @@ function run(args: string[] = [], env: Record<string, string> = {}): Run {PATH: process.env.PATH ?? '',OVHOST: ovhost,OVHOST_SUDO: '',-- DEPLOY_LEGACY: legacy,...env,},encoding: 'utf8',@@@ -74,32 +62,18 @@ describe('deploy/scripts/deploy.sh', () => {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' })++ it('exits with a clear error when ovhost is missing', () => {++ for (const args of [[], ['--rollback'], ['--wait-idle']]) {++ const r = run(args, { OVHOST: join(tmp, 'missing') })expect(r.code).toBe(1)expect(r.calls).toEqual([])++ expect(r.out).toMatch(/ovhost not found/)}})-- 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)-- }++ it('has no legacy deploy path', () => {++ expect(existsSync(join(ROOT, 'deploy', 'scripts', 'deploy-legacy.sh'))).toBe(false)expect(readFileSync(WRAPPER, 'utf8')).toMatch(/^set -euo pipefail$/m)++ expect(readFileSync(WRAPPER, 'utf8')).not.toMatch(/legacy|capabilities/i)})})ddeleted file mode 100755@@@ -1,35 +0,0 @@--#!/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@@@ -16,15 +16,9 @@# 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 pipefailSERVICE=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@@@ -39,30 +33,10 @@ while [ "$#" -gt 0 ]; doesacdone--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 ! command -v "$OVHOST" >/dev/null 2>&1; then++ echo "[games] ✗ ovhost not found ($OVHOST); install or configure ovhost before deploying" >&2++ exit 1++fiif [ "${DRY_RUN:-0}" = 1 ]; then exec "${SUDO[@]}" "$OVHOST" plan "$SERVICE"; fiecho "[games] ovhost $CMD $SERVICE ${FLAGS[*]:-}"
@@ -143,10 +143,7 @@ the restart (the SDK migrator applies any new migration as the owner before serv
@@ -143,10 +143,7 @@ the restart (the SDK migrator applies any new migration as the owner before serv
restored, reinstalled and rebuilt and the server restarted again. Players online are reported and
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`,
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
`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`).
+`--restart` if you did). The wrapper exits with an error if ovhost is missing.
Rollback: `sudo deploy/scripts/deploy.sh --rollback` (`ovhost rollback games`), which rebuilds the previous
Rollback: `sudo deploy/scripts/deploy.sh --rollback` (`ovhost rollback games`), which rebuilds the previous
commit. Migrations are additive and applied at boot, so an older build starts on the newer schema; the SDK
commit. Migrations are additive and applied at boot, so an older build starts on the newer schema; the SDK
@@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
import { spawnSync } from 'node:child_process'
import { spawnSync } from 'node:child_process'
-import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
+import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { fileURLToPath } from 'node:url'
@@ -7,9 +7,7 @@ import { afterAll, describe, expect, it } from 'vitest'
@@ -7,9 +7,7 @@ import { afterAll, describe, expect, it } from 'vitest'
/**
/**
* deploy/scripts/deploy.sh is a thin wrapper around `ovhost deploy games` (OpenVibe.Host, strategy
* 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.
+ * pnpm-build; roadmap WS-N task 11). A fake ovhost records what the wrapper asks for.
*/
*/
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
@@ -20,20 +18,11 @@ const ovhost = join(tmp, 'ovhost')
@@ -20,20 +18,11 @@ const ovhost = join(tmp, 'ovhost')
writeFileSync(
writeFileSync(
ovhost,
ovhost,
`#!/usr/bin/env bash
`#!/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}"
echo "ovhost $*" >> "${log}"
exit "\${FAKE_EXIT:-0}"
exit "\${FAKE_EXIT:-0}"
`,
`,
)
)
chmodSync(ovhost, 0o755)
chmodSync(ovhost, 0o755)
-const legacy = join(tmp, 'legacy.sh')
-writeFileSync(legacy, `#!/usr/bin/env bash\necho "legacy" >> "${log}"\n`)
-chmodSync(legacy, 0o755)
-
interface Run {
interface Run {
code: number | null
code: number | null
out: string
out: string
@@ -47,7 +36,6 @@ function run(args: string[] = [], env: Record<string, string> = {}): Run {
@@ -47,7 +36,6 @@ function run(args: string[] = [], env: Record<string, string> = {}): Run {
PATH: process.env.PATH ?? '',
PATH: process.env.PATH ?? '',
OVHOST: ovhost,
OVHOST: ovhost,
OVHOST_SUDO: '',
OVHOST_SUDO: '',
- DEPLOY_LEGACY: legacy,
...env,
...env,
},
},
encoding: 'utf8',
encoding: 'utf8',
@@ -74,32 +62,18 @@ describe('deploy/scripts/deploy.sh', () => {
@@ -74,32 +62,18 @@ describe('deploy/scripts/deploy.sh', () => {
expect(run(['--nope']).code).toBe(1)
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' })
+ it('exits with a clear error when ovhost is missing', () => {
+ for (const args of [[], ['--rollback'], ['--wait-idle']]) {
+ const r = run(args, { OVHOST: join(tmp, 'missing') })
expect(r.code).toBe(1)
expect(r.code).toBe(1)
expect(r.calls).toEqual([])
expect(r.calls).toEqual([])
+ expect(r.out).toMatch(/ovhost not found/)
}
}
})
})
- 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)
- }
+ it('has no legacy deploy path', () => {
+ expect(existsSync(join(ROOT, 'deploy', 'scripts', 'deploy-legacy.sh'))).toBe(false)
expect(readFileSync(WRAPPER, 'utf8')).toMatch(/^set -euo pipefail$/m)
expect(readFileSync(WRAPPER, 'utf8')).toMatch(/^set -euo pipefail$/m)
+ expect(readFileSync(WRAPPER, 'utf8')).not.toMatch(/legacy|capabilities/i)
})
})
})
})
deleted file mode 100755
deleted file mode 100755
@@ -1,35 +0,0 @@
@@ -1,35 +0,0 @@
-#!/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
@@ -16,15 +16,9 @@
@@ -16,15 +16,9 @@
# reported and reconnect (drain policy report); --wait-idle holds the restart until nobody plays. Do not pull
# 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).
# 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
set -euo pipefail
SERVICE=games
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}"
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
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
@@ -39,30 +33,10 @@ while [ "$#" -gt 0 ]; do
@@ -39,30 +33,10 @@ while [ "$#" -gt 0 ]; do
esac
esac
done
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 ! command -v "$OVHOST" >/dev/null 2>&1; then
+ echo "[games] ✗ ovhost not found ($OVHOST); install or configure ovhost before deploying" >&2
+ exit 1
+fi
if [ "${DRY_RUN:-0}" = 1 ]; then exec "${SUDO[@]}" "$OVHOST" plan "$SERVICE"; fi
if [ "${DRY_RUN:-0}" = 1 ]; then exec "${SUDO[@]}" "$OVHOST" plan "$SERVICE"; fi
echo "[games] ovhost $CMD $SERVICE ${FLAGS[*]:-}"
echo "[games] ovhost $CMD $SERVICE ${FLAGS[*]:-}"