Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ No ncurses. No browser DOM. No React. No native addon. No network access. Ever.
## Install

The `hqtui` CLI (widget showcase, `hqtui doctor`, the emoji font installer) in
one line. It uses bun if you have it, otherwise npm with Node 22.6+, and never
asks for sudo:
one line. It installs into `~/.local`, never asks for sudo, and fetches a private
Node if this machine has no Node 22+:

```sh
curl -fsSL https://hqtui.com/install.sh | sh
```

Run it again to update; remove with `sh ~/.local/share/hqtui/uninstall.sh`.

To build an app, add the library to your project:

```bash
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/docs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ npm add @profullstack/hqtui # Node 22.6+ works unchanged`} />
There is also a CLI: <code className="font-mono text-white/80">hqtui doctor</code>{" "}
reports what your terminal actually supports, and{" "}
<code className="font-mono text-white/80">hqtui</code> opens a built-in showcase.
Install it in one line (bun if present, otherwise npm with Node 22.6+, no sudo):
Install it in one line (into ~/.local, no sudo; fetches a private Node if you have no Node 22+):
</P>
<CommandBlock className="mt-3" command="curl -fsSL https://hqtui.com/install.sh | sh" label="CLI install" />

Expand Down
207 changes: 153 additions & 54 deletions apps/web/public/install.sh
Original file line number Diff line number Diff line change
@@ -1,60 +1,159 @@
#!/bin/sh
# Install the hqtui CLI (npm: @profullstack/hqtui): the widget showcase,
# `hqtui doctor` for terminal capabilities, and the OpenEmoji font installer.
# hqtui installer.
#
# curl -fsSL https://hqtui.com/install.sh | sh
#
# Uses bun if present, otherwise npm (Node >= 22.6). When the global npm
# prefix is not writable it installs into ~/.local instead of asking for sudo.
# Re-running it upgrades to the latest release.
set -eu

PKG="@profullstack/hqtui"
BIN="hqtui"

say() { printf '%s\n' "$*"; }
die() { printf 'hqtui install: %s\n' "$*" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }

say "Installing hqtui (High Quality Terminal UI)..."

if have bun; then
# bun add -g writes into the nearest package.json above its global dir when
# that dir has none (e.g. ~/package.json): give it its own first.
BUN_GLOBAL="${BUN_INSTALL:-$HOME/.bun}/install/global"
mkdir -p "$BUN_GLOBAL"
[ -f "$BUN_GLOBAL/package.json" ] || printf '{}\n' > "$BUN_GLOBAL/package.json"
bun add -g "$PKG@latest"
BIN_DIR="${BUN_INSTALL:-$HOME/.bun}/bin"
elif have npm && have node; then
node -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>22||(a===22&&b>=6)?0:1)' 2>/dev/null \
|| die "Node >= 22.6 required (found $(node -v)). Upgrade Node or install bun: https://bun.sh"
prefix=$(npm config get prefix)
modules="$prefix/lib/node_modules"
[ -d "$modules" ] || modules="$prefix/lib"
if [ -w "$modules" ] && [ -w "$prefix/bin" ]; then
npm install -g "$PKG@latest"
BIN_DIR="$prefix/bin"
# Installs @profullstack/hqtui (commands: hqtui) under your home directory. No root, no system
# package manager, no system files touched. If this machine has no Node 22+,
# an official Node build is fetched (checksum verified) into the same private directory
# and used from there. Run this again to update. Remove with:
# sh ~/.local/share/hqtui/uninstall.sh
#
# sh -s -- --version X install a specific release
# sh -s -- --prefix DIR install root (default: ~/.local)
#
# Listed in the W3BS catalog (https://w3bs.org/packages): `w3bs install hqtui` runs
# this same script and `w3bs update` re-runs it.
#
# Generated from the W3BS npm CLI installer template; keep the variables below in sync
# with package.json. Nothing runs until the last line, so a truncated download is inert.

main() {
set -eu
PKG="@profullstack/hqtui"
TOOL="hqtui"
URL="https://hqtui.com/install.sh"
NODE_MAJOR="22"
PREFIX="${INSTALL_PREFIX:-$HOME/.local}"
VERSION=""
NODE_CHOICE="${INSTALL_NODE:-auto}"

while [ $# -gt 0 ]; do
case "$1" in
--version) VERSION="${2:?--version needs a value}"; shift ;;
--prefix) PREFIX="${2:?--prefix needs a value}"; shift ;;
-h | --help) echo "Usage: curl -fsSL $URL | sh [-s -- --version X --prefix DIR]"; return 0 ;;
*) echo "$TOOL installer: unknown option $1" >&2; return 64 ;;
esac
shift
done

say() { printf '%s\n' "$*"; }
fail() { printf '%s installer: %s\n' "$TOOL" "$*" >&2; exit 1; }

case "$(uname -s)" in
Linux) OS=linux ;;
Darwin) OS=darwin ;;
*) fail "unsupported operating system $(uname -s); Linux and macOS are supported" ;;
esac
case "$(uname -m)" in
x86_64 | amd64) ARCH=x64 ;;
aarch64 | arm64) ARCH=arm64 ;;
*) fail "unsupported CPU $(uname -m)" ;;
esac
if command -v curl >/dev/null 2>&1; then
fetch() { curl --proto '=https' --tlsv1.2 -fsSL "$1" -o "$2"; }
elif command -v wget >/dev/null 2>&1; then
fetch() { wget --https-only -q "$1" -O "$2"; }
else
BIN_DIR="$HOME/.local/bin"
say "npm prefix $prefix is not writable; installing into $HOME/.local"
npm install -g --prefix "$HOME/.local" "$PKG@latest"
fail "curl or wget is required"
fi

SHARE="$PREFIX/share/$TOOL"
BIN="$PREFIX/bin"
mkdir -p "$SHARE" "$BIN"

node_major() { "$1" -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0; }
NODE=""
NODE_KIND=""
if [ "$NODE_CHOICE" != private ] && command -v node >/dev/null 2>&1 && [ "$(node_major node)" -ge "$NODE_MAJOR" ]; then
NODE="$(command -v node)"
NODE_KIND=system
elif [ -x "$SHARE/node/bin/node" ] && [ "$(node_major "$SHARE/node/bin/node")" -ge "$NODE_MAJOR" ]; then
NODE="$SHARE/node/bin/node"
NODE_KIND=private
fi
else
die "needs bun (https://bun.sh) or Node >= 22.6 with npm (https://nodejs.org)"
fi

say ""
if have "$BIN"; then
say "hqtui $("$BIN" --version 2>/dev/null || echo '') installed."
else
say "hqtui installed to $BIN_DIR, which is not on your PATH. Add it:"
say " export PATH=\"$BIN_DIR:\$PATH\""
fi
say ""
say "Next:"
say " hqtui # open the widget showcase"
say " hqtui doctor # what this terminal supports"
say " hqtui --help # every command"
say ""
say "Building an app? Add the library: bun add @profullstack/hqtui"
if [ -z "$NODE" ]; then
say "No Node $NODE_MAJOR+ here; fetching an official Node 24 into $SHARE/node ..."
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
fetch "https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt" "$TMP/SHASUMS256.txt" || fail "cannot reach nodejs.org"
TARBALL="$(grep -o "node-v24\.[0-9.]*-$OS-$ARCH\.tar\.gz" "$TMP/SHASUMS256.txt" | head -n 1 || true)"
[ -n "$TARBALL" ] || fail "nodejs.org has no Node 24 build for $OS-$ARCH"
fetch "https://nodejs.org/dist/latest-v24.x/$TARBALL" "$TMP/$TARBALL" || fail "cannot download $TARBALL"
if command -v sha256sum >/dev/null 2>&1; then SUM="$(sha256sum "$TMP/$TARBALL" | cut -d ' ' -f 1)"
else SUM="$(shasum -a 256 "$TMP/$TARBALL" | cut -d ' ' -f 1)"; fi
grep -q "^$SUM $TARBALL\$" "$TMP/SHASUMS256.txt" || fail "checksum mismatch for $TARBALL"
rm -rf "$SHARE/node" && mkdir -p "$SHARE/node"
tar -xzf "$TMP/$TARBALL" -C "$SHARE/node" --strip-components=1 || fail "cannot unpack $TARBALL"
NODE="$SHARE/node/bin/node"
NODE_KIND=private
fi
if [ "$NODE_KIND" = private ]; then NPM="$SHARE/node/bin/npm"; else NPM="$(dirname "$NODE")/npm"; [ -x "$NPM" ] || NPM=npm; fi
# npm runs package scripts with the Node in PATH; make that the chosen one.
PATH="$(dirname "$NODE"):$PATH"
export PATH

SPEC="$PKG"
[ -n "$VERSION" ] && SPEC="$PKG@$VERSION"
say "Installing $SPEC into $SHARE ..."
rm -rf "$SHARE/node_modules" "$SHARE/package.json" "$SHARE/package-lock.json"
cd "$SHARE"
printf '{ "name": "%s-install", "private": true }\n' "$TOOL" > package.json
"$NPM" install --silent --no-audit --no-fund --omit=dev "$SPEC" >/dev/null 2>&1 ||
"$NPM" install --no-audit --no-fund --omit=dev "$SPEC" || fail "npm could not install $SPEC"
PKG_DIR="$SHARE/node_modules/$PKG"
[ -f "$PKG_DIR/package.json" ] || fail "$SPEC installed but $PKG_DIR is missing"

# One shim per entry in the package's own `bin` map: a script, not a symlink, so the
# command always runs this copy with the chosen Node.
BINS="$("$NODE" -e '
const p = require(process.argv[1] + "/package.json");
const b = typeof p.bin === "string" ? { [p.name.split("/").pop()]: p.bin } : (p.bin || {});
for (const [k, v] of Object.entries(b)) if (/^[A-Za-z0-9._-]+$/.test(k)) console.log(k + "\t" + v);
' "$PKG_DIR")"
[ -n "$BINS" ] || fail "$SPEC declares no commands"
PATHS="$SHARE"
NAMES=""
OLDIFS="$IFS"
IFS='
'
for line in $BINS; do
name="${line%% *}"
rel="${line#* }"
entry="$PKG_DIR/$rel"
[ -f "$entry" ] || fail "$SPEC is missing $rel"
printf '#!/bin/sh\n# %s, installed by the %s installer (%s).\nexec "%s" "%s" "$@"\n' "$name" "$TOOL" "$URL" "$NODE" "$entry" > "$BIN/$name"
chmod 0755 "$BIN/$name"
PATHS="$PATHS
$BIN/$name"
NAMES="$NAMES $name"
done
IFS="$OLDIFS"

INSTALLED="$("$NODE" -p "require('$PKG_DIR/package.json').version" 2>/dev/null || echo unknown)"
{
printf '{\n "package": "%s",\n "version": "%s",\n "installer": "%s",\n' "$PKG" "$INSTALLED" "$URL"
printf ' "installedAt": "%s",\n "node": "%s",\n "paths": [\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$NODE_KIND"
printf '%s\n' "$PATHS" | sed 's/.*/ "&",/' | sed '$ s/,$//'
printf ' ]\n}\n'
} > "$SHARE/manifest.json"
{
echo '#!/bin/sh'
echo "# Removes $TOOL. Written by its installer."
echo 'set -eu'
printf '%s\n' "$PATHS" | sed 's|.*|rm -rf "&"|'
echo "echo '$TOOL removed.'"
} > "$SHARE/uninstall.sh"
chmod 0755 "$SHARE/uninstall.sh"

say ""
say "Installed $PKG $INSTALLED:$NAMES (in $BIN)"
case ":$PATH:" in
*":$BIN:"*) ;;
*) say "Add $BIN to your PATH: echo 'export PATH=\"$BIN:\$PATH\"' >> ~/.profile" ;;
esac
say "Update by running the installer again; remove with: sh $SHARE/uninstall.sh"
}

main "$@"
Loading
Loading