From bd34ec49aacdc8f4524d298035bf6d0d2ac89c44 Mon Sep 17 00:00:00 2001 From: Raven Scott Date: Fri, 11 Sep 2026 21:28:44 -0400 Subject: [PATCH] Update Installer --- README.md | 5 ++- docs/MIGRATIONS.md | 12 ++++-- docs/operations.md | 8 ++-- packaging/install.sh | 73 ++++++++++++++++++++++++++++----- test/install.test.js | 98 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 test/install.test.js diff --git a/README.md b/README.md index c6334ca..1be942e 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,10 @@ To remove Jarvis-owned memory, cache, and generated media as well: bash packaging/uninstall.sh --yes --purge-data ~~~ -Read [Migration notes](docs/MIGRATIONS.md) before upgrading or moving an +Re-running the installer upgrades in place: it stops the old daemon, removes +the previous application and extension files, then copies the new tree. +Config, memory, and model cache are kept. Read +[Migration notes](docs/MIGRATIONS.md) before upgrading or moving an installation. ## Documentation diff --git a/docs/MIGRATIONS.md b/docs/MIGRATIONS.md index d596fb9..72f0031 100644 --- a/docs/MIGRATIONS.md +++ b/docs/MIGRATIONS.md @@ -6,9 +6,15 @@ The user installer keeps application files in `~/.config/jarvis/qvac.config.json`. Memory, voice references, audit logs, and generated media remain under their existing Jarvis data directories. -Upgrades replace only the application and extension files. Existing config and -data are preserved. Run `systemctl --user daemon-reload` after installing a new -service file, then restart `jarvisd.service`. +Upgrades stop a running `jarvisd.service`, uninstall the previous GNOME +extension, and replace `~/.local/share/jarvis-qvac` from a staging directory so +leftover files from older versions cannot mix with the new tree. Failed staging +directories next to the install prefix are removed first. Existing config and +data under `~/.config/jarvis`, `~/.local/share/jarvis`, and the model cache are +preserved. If the service was running, the installer restarts it after the +replacement; `--enable` also enables the unit. Run +`systemctl --user daemon-reload` only when you install the service file by +hand. The uninstall command requires `--yes` and preserves data. Add `--purge-data` only when the user explicitly wants Jarvis-owned memory, cache, diff --git a/docs/operations.md b/docs/operations.md index c3d5dfe..6dc2cfd 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -16,9 +16,11 @@ curl -fsSL https://git.ssh.surf/snxraven/gnome-jarvis/raw/branch/main/packaging/ ~~~ The installer downloads the self-contained Bare bundle from the `rolling` -release and delegates to the repository installer. Omit `--enable` to copy -files without starting the service. Use `JARVIS_SERVER`, `JARVIS_REPO`, and -`JARVIS_BUNDLE_URL` for a mirror. +release and delegates to the repository installer. Re-running it replaces the +previous user-local application and extension trees after stopping +`jarvisd.service`; config and data stay in place. Omit `--enable` to copy +files without starting the service unless the daemon was already running. +Use `JARVIS_SERVER`, `JARVIS_REPO`, and `JARVIS_BUNDLE_URL` for a mirror. ## Development start diff --git a/packaging/install.sh b/packaging/install.sh index 8cdd29c..36d97ce 100755 --- a/packaging/install.sh +++ b/packaging/install.sh @@ -6,12 +6,58 @@ APP_DIR="${HOME}/.local/share/jarvis-qvac" EXT_DIR="${XDG_DATA_HOME:-${HOME}/.local/share}/gnome-shell/extensions/jarvis@qvac.local" CONFIG_DIR="${HOME}/.config/jarvis" CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/jarvis/models" -mkdir -p "${APP_DIR}" "${EXT_DIR}" "${CONFIG_DIR}" "${HOME}/.config/systemd/user" "${CACHE_DIR}" -if [[ -f "${ROOT_DIR}/node_modules/bare-runtime-linux-x64/bin/bare" || -f "${ROOT_DIR}/node_modules/bare-runtime-linux-arm64/bin/bare" ]]; then - tar --exclude='.git' --exclude='./.agents' --exclude='./.codex' --exclude='./dist' --exclude='./vendor/agent-harness/node_modules' --exclude='*/__pycache__' --exclude='*.pyc' -cf - -C "${ROOT_DIR}" . | tar -xf - -C "${APP_DIR}" -else - tar --exclude='.git' --exclude='./.agents' --exclude='./.codex' --exclude='./dist' --exclude='./node_modules' --exclude='*/node_modules' --exclude='*/__pycache__' --exclude='*.pyc' -cf - -C "${ROOT_DIR}" . | tar -xf - -C "${APP_DIR}" +APP_PARENT="$(dirname "${APP_DIR}")" +mkdir -p "${APP_PARENT}" "${CONFIG_DIR}" "${HOME}/.config/systemd/user" "${CACHE_DIR}" + +STAGING="" +EXT_TMP_DIR="" +cleanup_install() { rm -rf "${STAGING}" "${EXT_TMP_DIR}"; } +trap cleanup_install EXIT + +has_bundled_bare() { + [[ -f "${ROOT_DIR}/node_modules/bare-runtime-linux-x64/bin/bare" || -f "${ROOT_DIR}/node_modules/bare-runtime-linux-arm64/bin/bare" ]] +} + +extract_tree() { + local dest="$1" + mkdir -p "${dest}" + if has_bundled_bare; then + tar --exclude='.git' --exclude='./.agents' --exclude='./.codex' --exclude='./dist' --exclude='./vendor/agent-harness/node_modules' --exclude='*/__pycache__' --exclude='*.pyc' -cf - -C "${ROOT_DIR}" . | tar -xf - -C "${dest}" + else + tar --exclude='.git' --exclude='./.agents' --exclude='./.codex' --exclude='./dist' --exclude='./node_modules' --exclude='*/node_modules' --exclude='*/__pycache__' --exclude='*.pyc' -cf - -C "${ROOT_DIR}" . | tar -xf - -C "${dest}" + fi +} + +PREVIOUSLY_ACTIVE=false +if command -v systemctl >/dev/null; then + if systemctl --user is-active --quiet jarvisd.service 2>/dev/null; then + PREVIOUSLY_ACTIVE=true + systemctl --user stop jarvisd.service || true + fi fi + +if [[ -e "${APP_DIR}" || -e "${EXT_DIR}" ]]; then + echo "Cleaning the previous JARVIS-QVAC install so this upgrade is a clean replacement." +fi + +if command -v gnome-extensions >/dev/null; then + gnome-extensions disable jarvis@qvac.local >/dev/null 2>&1 || true + gnome-extensions uninstall jarvis@qvac.local >/dev/null 2>&1 || true +fi +rm -rf "${EXT_DIR}" + +shopt -s nullglob +for leftover in "${APP_PARENT}"/jarvis-qvac.staging.*; do + rm -rf "${leftover}" +done +shopt -u nullglob + +STAGING="$(mktemp -d "${APP_PARENT}/jarvis-qvac.staging.XXXXXX")" +extract_tree "${STAGING}" +rm -rf "${APP_DIR}" +mv "${STAGING}" "${APP_DIR}" +STAGING="" + case "$(uname -m)" in x86_64|amd64) BARE_BIN="${APP_DIR}/node_modules/bare-runtime-linux-x64/bin/bare" ;; aarch64|arm64) BARE_BIN="${APP_DIR}/node_modules/bare-runtime-linux-arm64/bin/bare" ;; @@ -25,11 +71,8 @@ fi chmod 755 "${BARE_BIN}" EXT_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/jarvis-extension.XXXXXX")" EXT_ZIP="${EXT_TMP_DIR}/jarvis-extension.zip" -cleanup_ext() { rm -rf "${EXT_TMP_DIR}"; } -trap cleanup_ext EXIT -( cd "${ROOT_DIR}/apps/gnome-extension/jarvis@qvac.local" && zip -q -r "${EXT_ZIP}" . ) -rm -rf "${EXT_DIR}" mkdir -p "${EXT_DIR}" +( cd "${ROOT_DIR}/apps/gnome-extension/jarvis@qvac.local" && zip -q -r "${EXT_ZIP}" . ) if command -v gnome-extensions >/dev/null; then if ! gnome-extensions install --force "${EXT_ZIP}"; then echo "GNOME extension registration failed; installing the files directly." >&2 @@ -45,10 +88,15 @@ sed "s#__JARVIS_BARE__#${BARE_BIN}#" "${ROOT_DIR}/packaging/jarvisd.service" > " if [[ ! -f "${CONFIG_DIR}/qvac.config.json" ]]; then sed "s#__JARVIS_CACHE__#${CACHE_DIR}#g" "${ROOT_DIR}/packaging/qvac.config.template.json" > "${CONFIG_DIR}/qvac.config.json" fi -if [[ "${1:-}" == "--enable" ]]; then +restart_user_service() { systemctl --user daemon-reload - systemctl --user enable jarvisd.service + if [[ "${1:-}" == "enable" ]]; then + systemctl --user enable jarvisd.service + fi systemctl --user restart jarvisd.service +} +if [[ "${1:-}" == "--enable" ]]; then + restart_user_service enable if command -v gnome-extensions >/dev/null && gnome-extensions enable jarvis@qvac.local >/dev/null 2>&1; then echo "GNOME extension enabled." elif command -v gsettings >/dev/null && gsettings writable org.gnome.shell enabled-extensions >/dev/null 2>&1; then @@ -67,6 +115,9 @@ if [[ "${1:-}" == "--enable" ]]; then else echo "GNOME Shell has not rescanned extensions; log out and back in, then run: gnome-extensions enable jarvis@qvac.local" fi +elif [[ "${PREVIOUSLY_ACTIVE}" == true ]]; then + restart_user_service + echo "Restarted jarvisd.service with the upgraded files." fi echo "Installed JARVIS-QVAC to ${APP_DIR}" echo "Run: ${APP_DIR}/packaging/first-run.sh" diff --git a/test/install.test.js b/test/install.test.js new file mode 100644 index 0000000..0df258a --- /dev/null +++ b/test/install.test.js @@ -0,0 +1,98 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { chmod, cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repo = path.resolve(fileURLToPath(new URL('..', import.meta.url))); + +function run(command, args, env) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { env, cwd: repo }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (chunk) => { stdout += chunk; }); + child.stderr.on('data', (chunk) => { stderr += chunk; }); + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) resolve({ stdout, stderr }); + else reject(new Error(`install failed (${code}): ${stderr || stdout}`)); + }); + }); +} + +test('install.sh replaces leftover files from an older version and keeps config', async () => { + const work = await mkdtemp(path.join(os.tmpdir(), 'jarvis-install-')); + const home = path.join(work, 'home'); + const root = path.join(work, 'src'); + const bin = path.join(work, 'bin'); + const log = path.join(work, 'commands.log'); + const machine = (await run('uname', ['-m'], process.env)).stdout.trim(); + const bareName = /aarch64|arm64/.test(machine) ? 'bare-runtime-linux-arm64' : 'bare-runtime-linux-x64'; + try { + await mkdir(path.join(root, 'packaging'), { recursive: true }); + await mkdir(path.join(root, 'apps/gnome-extension'), { recursive: true }); + await mkdir(path.join(root, 'node_modules', bareName, 'bin'), { recursive: true }); + await mkdir(bin, { recursive: true }); + await cp(path.join(repo, 'packaging/install.sh'), path.join(root, 'packaging/install.sh')); + await cp(path.join(repo, 'packaging/jarvisd.service'), path.join(root, 'packaging/jarvisd.service')); + await cp(path.join(repo, 'packaging/qvac.config.template.json'), path.join(root, 'packaging/qvac.config.template.json')); + await cp(path.join(repo, 'apps/gnome-extension/jarvis@qvac.local'), path.join(root, 'apps/gnome-extension/jarvis@qvac.local'), { recursive: true }); + await writeFile(path.join(root, 'node_modules', bareName, 'bin', 'bare'), '#!/bin/sh\nexit 0\n'); + await chmod(path.join(root, 'node_modules', bareName, 'bin', 'bare'), 0o755); + await writeFile(path.join(root, 'daemon-marker'), 'new-tree\n'); + + const appDir = path.join(home, '.local/share/jarvis-qvac'); + const extDir = path.join(home, '.local/share/gnome-shell/extensions/jarvis@qvac.local'); + const configDir = path.join(home, '.config/jarvis'); + await mkdir(path.join(appDir, 'node_modules/old-pkg'), { recursive: true }); + await mkdir(extDir, { recursive: true }); + await mkdir(configDir, { recursive: true }); + await mkdir(path.join(home, '.local/share/jarvis-qvac.staging.oldfail'), { recursive: true }); + await writeFile(path.join(appDir, 'stale-old-version.js'), 'leftover from previous install\n'); + await writeFile(path.join(extDir, 'old-extension.js'), 'leftover extension\n'); + await writeFile(path.join(configDir, 'config.json'), '{"wakePhrase":"hey jarvis","ttsEnabled":true}\n'); + await writeFile(path.join(home, '.local/share/jarvis-qvac.staging.oldfail/junk'), 'failed staging\n'); + + await writeFile(path.join(bin, 'systemctl'), `#!/bin/sh +echo "systemctl $*" >> '${log}' +if [ "$2" = is-active ]; then exit 0; fi +exit 0 +`); + await writeFile(path.join(bin, 'gnome-extensions'), `#!/bin/sh +echo "gnome-extensions $*" >> '${log}' +if [ "$1" = install ]; then exit 1; fi +exit 0 +`); + await chmod(path.join(bin, 'systemctl'), 0o755); + await chmod(path.join(bin, 'gnome-extensions'), 0o755); + + await run('bash', [path.join(root, 'packaging/install.sh'), '--enable'], { + ...process.env, + HOME: home, + PATH: `${bin}:${process.env.PATH}`, + XDG_DATA_HOME: path.join(home, '.local/share'), + XDG_CACHE_HOME: path.join(home, '.cache'), + }); + + assert.equal(existsSync(path.join(appDir, 'stale-old-version.js')), false); + assert.equal(existsSync(path.join(appDir, 'node_modules/old-pkg')), false); + assert.equal(existsSync(path.join(home, '.local/share/jarvis-qvac.staging.oldfail')), false); + assert.equal(existsSync(path.join(extDir, 'old-extension.js')), false); + assert.equal(existsSync(path.join(extDir, 'extension.js')), true); + assert.equal(await readFile(path.join(appDir, 'daemon-marker'), 'utf8'), 'new-tree\n'); + assert.equal(await readFile(path.join(configDir, 'config.json'), 'utf8'), '{"wakePhrase":"hey jarvis","ttsEnabled":true}\n'); + const unit = await readFile(path.join(home, '.config/systemd/user/jarvisd.service'), 'utf8'); + assert.match(unit, /bare-runtime-linux/); + assert.doesNotMatch(unit, /\/usr\/bin\/node/); + const commands = await readFile(log, 'utf8'); + assert.match(commands, /systemctl --user stop jarvisd\.service/); + assert.match(commands, /gnome-extensions uninstall jarvis@qvac\.local/); + assert.match(commands, /systemctl --user restart jarvisd\.service/); + } finally { + await rm(work, { recursive: true, force: true }); + } +});