Update Installer
Rolling release / release (push) Canceled after 6m21s

This commit is contained in:
2026-09-11 21:28:44 -04:00
parent 9e76928045
commit bd34ec49aa
5 changed files with 178 additions and 18 deletions
+4 -1
View File
@@ -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
+9 -3
View File
@@ -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,
+5 -3
View File
@@ -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
+61 -10
View File
@@ -6,12 +6,58 @@ APP_DIR="${HOME}/.local/share/jarvis-qvac"
EXT_DIR="${XDG_DATA_HOME:-${HOME}/.local/share}/gnome-shell/extensions/[email protected]"
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 [email protected] >/dev/null 2>&1 || true
gnome-extensions uninstall [email protected] >/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/[email protected]" && zip -q -r "${EXT_ZIP}" . )
rm -rf "${EXT_DIR}"
mkdir -p "${EXT_DIR}"
( cd "${ROOT_DIR}/apps/gnome-extension/[email protected]" && 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
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 [email protected] >/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 [email protected]"
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"
+98
View File
@@ -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/[email protected]'), path.join(root, 'apps/gnome-extension/[email protected]'), { 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/[email protected]');
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 });
}
});