From 9249a928cb77518842ff0937cda632d0df6ea9b6 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 6 Oct 2026 15:52:50 +0000 Subject: [PATCH 1/2] hqterm update: self-update hqterm, hqsh and the desktop app (0.2.3) hqterm update fetches the installer from hqterm.sh and runs it into the running binary's directory, with --desktop when the app is installed; --check reports whether a release is waiting, --force reinstalls. Co-Authored-By: Claude Opus 5.5 --- desktop/package.json | 2 +- package.json | 2 +- src/main.ts | 6 ++++ src/update.ts | 86 ++++++++++++++++++++++++++++++++++++++++++++ test/update.test.ts | 24 +++++++++++++ 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/update.ts create mode 100644 test/update.test.ts diff --git a/desktop/package.json b/desktop/package.json index 24534d8..9266f50 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "hqterm-desktop", "productName": "hqterm", - "version": "0.2.2", + "version": "0.2.3", "description": "hqterm desktop: a terminal window that shows hqtui/qc images and HD emoji (xterm.js + node-pty).", "license": "MIT", "author": "Profullstack, Inc. ", diff --git a/package.json b/package.json index b6a0110..d6b14e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hqterm", - "version": "0.2.2", + "version": "0.2.3", "description": "Terminal session manager: persistent hqsh sessions with images and HD emoji, built on hqtui.", "license": "MIT", "private": true, diff --git a/src/main.ts b/src/main.ts index 5db3a7c..5bdc7bd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { emojiFontStatus, installEmojiFont, removeEmojiFont } from "@profullstack/hqtui"; import { desktop } from "./desktop.ts"; import { doctor } from "./doctor.ts"; +import { update } from "./update.ts"; import { addUserHost, loadUserHosts, mergeHosts, validHostName } from "./hosts.ts"; import { connect, findHqsh, installArgs, INSTALL_URL, pressEnter, runInteractive, connectArgs } from "./run.ts"; import { sshConfigHosts } from "./sshconfig.ts"; @@ -25,6 +26,8 @@ Usage: what this terminal supports, with a test image hqterm fonts install|status|remove the OpenEmoji colour font for this terminal + hqterm update [--check] [--force] + update hqterm, hqsh and the desktop app hqterm --version | --help Keys in the manager: @@ -129,6 +132,9 @@ export async function main(argv: string[]): Promise { return doctor(rest); case "fonts": return fonts(rest[0]); + case "update": + case "upgrade": + return update(rest); default: console.error(`hqterm: unknown command "${cmd}"\n`); process.stderr.write(HELP); diff --git a/src/update.ts b/src/update.ts new file mode 100644 index 0000000..a58fba9 --- /dev/null +++ b/src/update.ts @@ -0,0 +1,86 @@ +/** + * `hqterm update`: fetch the current installer from hqterm.sh and run it, so + * hqterm, hqsh (and the desktop app, when it is installed) move to the latest + * releases. `hqterm update --check` only reports whether one is waiting. + */ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { findDesktop } from "./desktop.ts"; +import { INSTALL_URL, runInteractive } from "./run.ts"; +import { VERSION } from "./version.ts"; + +export const RELEASES_API = "https://api.github.com/repos/profullstack/hqterm/releases/latest"; + +/** -1, 0 or 1 comparing dotted versions ("0.2.10" > "0.2.9"). */ +export function compareVersions(a: string, b: string): number { + const pa = a.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0); + const pb = b.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const d = (pa[i] ?? 0) - (pb[i] ?? 0); + if (d) return d > 0 ? 1 : -1; + } + return 0; +} + +/** The installer's arguments for this machine: same bin dir, desktop app if installed. */ +export function installerArgs(execPath: string, hasDesktop: boolean, extra: string[] = []): string[] { + const args: string[] = []; + // A compiled binary updates in place; under `bun src/main.ts` use the default dir. + if (!/\bbun(\.exe)?$/.test(execPath)) args.push(`--bin=${dirname(execPath)}`); + if (hasDesktop) args.push("--desktop"); + return [...args, ...extra]; +} + +export async function latestVersion(fetchImpl: typeof fetch = fetch): Promise { + try { + const res = await fetchImpl(RELEASES_API, { headers: { Accept: "application/vnd.github+json", "User-Agent": "hqterm" } }); + if (!res.ok) return null; + const body = (await res.json()) as { tag_name?: string }; + return body.tag_name ? body.tag_name.replace(/^v/, "") : null; + } catch { + return null; + } +} + +export async function update(args: string[]): Promise { + const check = args.includes("--check"); + const latest = await latestVersion(); + if (check) { + if (!latest) { + console.error("hqterm: could not reach GitHub to check for updates."); + return 1; + } + if (compareVersions(latest, VERSION) > 0) { + console.log(`hqterm ${VERSION} -> ${latest} is available. Run: hqterm update`); + return 0; + } + console.log(`hqterm ${VERSION} is the latest.`); + return 0; + } + if (latest && compareVersions(latest, VERSION) <= 0 && !args.includes("--force")) { + console.log(`hqterm ${VERSION} is the latest. (hqterm update --force reinstalls, and also updates hqsh and the desktop app.)`); + return 0; + } + + const res = await fetch(INSTALL_URL).catch(() => null); + if (!res?.ok) { + console.error(`hqterm: could not download ${INSTALL_URL}.`); + return 1; + } + const script = await res.text(); + if (!script.startsWith("#!")) { + console.error(`hqterm: ${INSTALL_URL} did not return an installer.`); + return 1; + } + const dir = mkdtempSync(join(tmpdir(), "hqterm-update-")); + const file = join(dir, "install.sh"); + writeFileSync(file, script, { mode: 0o700 }); + try { + const extra = args.filter((a) => a !== "--force"); + console.log(`Updating hqterm ${VERSION}${latest ? ` -> ${latest}` : ""}...`); + return await runInteractive(["sh", file, ...installerArgs(process.execPath, !!findDesktop(), extra)]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} diff --git a/test/update.test.ts b/test/update.test.ts new file mode 100644 index 0000000..6bde414 --- /dev/null +++ b/test/update.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test"; +import { compareVersions, installerArgs, latestVersion } from "../src/update.ts"; + +describe("hqterm update", () => { + test("compares versions numerically", () => { + expect(compareVersions("0.2.10", "0.2.9")).toBe(1); + expect(compareVersions("v0.2.2", "0.2.2")).toBe(0); + expect(compareVersions("0.1.0", "0.2.2")).toBe(-1); + }); + + test("updates a compiled binary in place and keeps the desktop app", () => { + expect(installerArgs("/home/a/.local/bin/hqterm", true)).toEqual(["--bin=/home/a/.local/bin", "--desktop"]); + expect(installerArgs("/usr/local/bin/hqterm", false)).toEqual(["--bin=/usr/local/bin"]); + // Running from source under bun: the installer's default directory. + expect(installerArgs("/home/a/.bun/bin/bun", false)).toEqual([]); + }); + + test("reads the latest release tag, and says nothing when GitHub fails", async () => { + const ok = (async () => new Response(JSON.stringify({ tag_name: "v0.2.3" }))) as unknown as typeof fetch; + expect(await latestVersion(ok)).toBe("0.2.3"); + const down = (async () => new Response("", { status: 503 })) as unknown as typeof fetch; + expect(await latestVersion(down)).toBeNull(); + }); +}); From df86fbf5d8995e10ce3f3173731b24de8ebd8b97 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 6 Oct 2026 15:52:57 +0000 Subject: [PATCH 2/2] desktop lockfile: 0.2.3 Co-Authored-By: Claude Opus 5.5 --- desktop/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index f7103ed..1e8eac7 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "hqterm-desktop", - "version": "0.2.2", + "version": "0.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hqterm-desktop", - "version": "0.2.2", + "version": "0.2.3", "license": "MIT", "dependencies": { "@xterm/addon-fit": "^0.11.0",