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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ On hdtilt.com an account comes first: sign up (username, email, password), confi
curl -fsSL https://hdtilt.com/install.sh | sh
```

That installs the `hdtilt` command with bun, or with npm (Node >= 22.6), into `~/.local` when the
global prefix needs root. On a Linux desktop it also puts the desktop app (AppImage) in `~/.local`
with a menu entry; pass `sh -s -- --cli-only` to skip it, or `--desktop` to force it. Re-run to upgrade.
That installs the `hdtilt` command into `~/.local` (no root). It uses your Node 22+ if you have
one, otherwise it fetches an official Node build (checksum verified) into `~/.local/share/hdtilt`.
Re-run it to upgrade; `sh -s -- --version X` pins a release; remove it with
`sh ~/.local/share/hdtilt/uninstall.sh`. The desktop app (AppImage, deb, dmg, exe) is on the
[releases page](https://github.com/profullstack/hdtilt.com/releases/latest).
Or install the package yourself:

```sh
Expand Down
159 changes: 159 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/bin/sh
# hdtilt installer.
#
# curl -fsSL https://hdtilt.com/install.sh | sh
#
# Installs hdtilt (commands: hdtilt) 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/hdtilt/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 hdtilt` 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="hdtilt"
TOOL="hdtilt"
URL="https://hdtilt.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
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
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