feat(install): curl|sh installer script (ADR-0055)
ci / gate (push) Successful in 3m19s
website / deploy (push) Successful in 1m58s

scripts/install.sh — POSIX sh, shellcheck-clean: detects uname OS/arch ->
target triple (Linux uses the static musl build; Windows rejected with a
Scoop/winget pointer), resolves the latest release (or RDBMS_VERSION),
downloads the asset + its .sha256 and verifies it, installs to
~/.local/bin with a PATH hint. RDBMS_OS/RDBMS_ARCH + --print-target are
testing seams. Verified end-to-end against the live public v0.1.0 (all
mappings, pinned + latest, checksum incl. tamper-rejection, install+run).

ADR-0055 + README index; plan-doc step 2 done + decisions recorded
(crates.io=yes, releases public, tracking via doc+ADR).
This commit is contained in:
claude@clouddev1
2026-06-17 19:41:34 +00:00
parent c30a6114b9
commit ef99e6c676
4 changed files with 305 additions and 28 deletions
+75
View File
@@ -0,0 +1,75 @@
# ADR-0055: `curl | sh` install script (`scripts/install.sh`)
## Status
Accepted — **implemented 2026-06-17** (plan:
`docs/plans/20260616-public-availability.md`, step 2). Step 2 on the road
to public availability, building on ADR-0054 (versioned releases) and
ADR-ci-001/003 (the Gitea releases it downloads from). Tracked by the
plan + this ADR (no Gitea issue — user decision, 2026-06-17).
## Context
Until now, installing meant: find the releases page, work out which of
the six assets matches your machine, download it, `chmod +x`, move it
onto `PATH`, and (on macOS) wonder about Gatekeeper. That is too much
friction for a teaching tool aimed at beginners. The Gitea releases are
**publicly downloadable** (confirmed), with deterministic asset names
(`rdbms-playground-<tag>-<target>[.exe]`) and `.sha256` sidecars
(ADR-ci-003), and a `releases/latest` API — enough to script a one-liner
install.
## Decision
Ship **`scripts/install.sh`**, run as
`curl -fsSL <gitea-raw>/scripts/install.sh | sh`:
- **POSIX `sh`** (no bashisms) — it runs under the `sh` of `curl | sh`;
kept **shellcheck-clean** (`-s sh`).
- **Platform detection** from `uname` → target triple: Linux →
`<arch>-unknown-linux-musl` (the fully-static build — one universal
Linux artifact, no glibc/version coupling), macOS → `<arch>-apple-darwin`;
`x86_64`/`amd64` and `aarch64`/`arm64` both map. **Windows is rejected**
with a pointer to Scoop/winget/the releases page (the binary is a `.exe`,
not a `curl|sh` target).
- **Version:** the `releases/latest` API tag by default; `RDBMS_VERSION`
pins a specific tag.
- **Integrity:** always download the `.sha256` sidecar and **verify**
(`sha256sum`/`shasum -a 256`); a mismatch aborts the install. HTTPS only.
- **Install location:** `~/.local/bin` by default (user-writable, no
sudo), overridable via `RDBMS_INSTALL_DIR`; prints a PATH hint if the
dir isn't on `PATH`.
- **macOS note:** a `curl` download is **not** Gatekeeper-quarantined, so
the binary runs as-is even while it is only ad-hoc-signed; proper
Developer-ID signing + notarization (for *browser* downloads) is a
separate, postponed task (see the plan's signing item).
- **Testing seams:** `RDBMS_OS`/`RDBMS_ARCH` force detection and
`--print-target` prints the resolved triple and exits — so the mapping
is checkable without a download.
### Rejected / deferred
- **Hosting the script on the website domain** (Cloudflare): nicer URL,
but adds a moving part; the **Gitea repo raw URL** is simplest and the
binaries live there anyway (user decision). The website may later
*reference* the same command.
- **`install.ps1` (Windows):** deferred — Windows users go via Scoop /
winget (D3, §3).
- **Uploading `install.sh` as a release asset** for a stable link:
optional; the branch raw URL is fine for now.
## Consequences
- A first-time user runs one line and gets a checksum-verified binary on
`PATH`. The website's install copy (website branch, separate agent) can
point at this command.
- **Verified end-to-end** (2026-06-17) against the live public `v0.1.0`:
all four Linux/macOS platform mappings + Windows/unknown-arch rejection;
pinned and latest paths; checksum verification incl. a tamper-rejection
check; install + run on Linux x86_64. (The installed `v0.1.0` predates
`--version`, ADR-0054 — a non-issue, and the reason to cut a new
release.)
- **No automated regression guard in CI yet:** shellcheck isn't in the
flake, and there's no shell-test harness here (no bats). Recommended
follow-up: add a `shellcheck scripts/*.sh` gate (touches ADR-ci-002 —
needs shellcheck in the devShell). For now the guard is local
shellcheck + the documented end-to-end verification.
+1
View File
@@ -67,3 +67,4 @@ This directory contains the project's ADRs, recorded per
- [ADR-0052 — Mode-tagged history for cross-mode recall](0052-mode-tagged-history-cross-mode-recall.md) — **Accepted + implemented 2026-06-13 (issue #30)**, closes Gitea **#30** — the feature (advanced history reusable in simple mode) **and** the bug in its comment (the `:` one-shot prefix lost across sessions). **Amends ADR-0034** (status field gains a `:adv` tag; **journaling moves from the worker to the dispatch layer**), **ADR-0015 §5/§6** (history.log leaves the worker transaction — `commit-db-last` now scopes yaml/csv/db only), and **ADR-0040** (a success-path journal-write failure is best-effort, not fatal); references ADR-0003. **Root cause:** history carried no mode, and the in-memory ring stored the raw `:select 1` while the worker journalled the *stripped* `select 1`, so the `:` was lost on disk. **Fix:** record the submission mode per entry as a **`:adv` suffix on the status token** (`ok`/`ok:adv`/`err`/`err:adv`) — `source` stays last + canonical so replay is unaffected; the in-memory ring (still `Vec<String>`) stores advanced entries in their `: `-prefixed simple-mode runnable form (a leading `:` unambiguously marks advanced since simple DSL never starts with `:`); recall **strips the `:` in advanced mode** (runs as bare SQL) and keeps it in simple mode (runs via the one-shot escape); hydration reconstructs the `: `-prefix from the tag, so cross-session = in-session. **The architectural turn (user's call):** the first draft kept journaling in the worker + threaded the mode down (~30-site plumbing); on review the user asked why the journal is written deep in the worker when the *failure* path already journals at the top of the chain — it shouldn't (history.log is a journal, not state). So **success journaling moved up** to `spawn_dsl_dispatch` / `run_replay` / the app-command sites (next to the failure path), the worker's `finalize_persistence` now writes only yaml/csv, and the journal write became **best-effort** (the command is already committed — consistent with the failure path; a rare disk-full leaves a committed command unjournalled, state intact). **App commands** journal simple (dispatched outside the spawn) and `submit` excludes them from the ring's advanced flag, so `undo`/`mode advanced` recall bare. Forks user-chosen: status-tag format (vs 4th field / `:`-in-source); unified scope; **dispatch-layer best-effort journaling** (vs worker-coupled-fatal). Two `/runda` passes (the second drove the relocation + app-command exclusion). Tests: the 15 worker-level journaling tests retired (worker no longer journals — yaml/csv/operation checks kept), re-covered at the new layer (history.rs status-tag + `:`-reconstruct; app.rs recall matrix; the #30 cross-session regression in `iteration6`; replay tests cover `run_replay` journaling). **2471 pass / 0 fail / 0 skip (1 ignored), clippy clean.** replay re-journaling mode-fidelity (a replayed advanced line re-journals simple — not a regression). **Follow-up done 2026-06-14:** the vestigial worker `source` plumbing was fully unwound (compiler-guided, no behaviour change) — `_source` removed from `finalize_persistence`/`do_rebuild_from_text`, the three `*_request` wrappers inlined+deleted, the dead `source` param dropped from the ~30 forwarding worker handlers, and the `source` field removed from the `DescribeTable`/`QueryData`/`RunSelect` requests + their `DatabaseHandle` methods (~164 mostly-test call sites); the only worker `source` left is the snapshot/undo label (see ADR-0052 *Consequences*)
- [ADR-0053 — Contextual `hint` command and keybinding](0053-contextual-hint-command-and-keybinding.md) — **Accepted, implemented 2026-06-15** (Phases AD; closes **A1** + requirements **H2**). Settles the `hint` slot ADR-0003 left "ADR pending"; closes the last open piece of **A1** and tracks requirements **H2**. **Two surfaces:** an **F1 keybinding** that renders a deep hint for the *live* partial input without submitting (the primary path — a submitted `hint` command can't see the buffer it would help with, since Enter empties it), and a submitted **`hint` command** that expands on the *most recent error*. **No topic argument** (contextual only — `help <topic>` already owns explicit reference). Introduces a **tier-3 teaching layer**, deeper than the existing tier-1 (colour / error headline) and tier-2 (ambient one-liner; and the error `hint:`, which is shown **by default** since `Verbosity::Verbose` is the default — `messages short` is the opt-*out*); without it `hint` would just duplicate what's already on screen. Tier-3 content lives in the catalogue under `hint.cmd.<hint_id>` (per command form) and `hint.err.<class>` (per error/diagnostic class), each a structured `what`/`example`/`concept` block rendered via a new `note_hint*` family with `OutputStyleClass::Hint`. **Keyed per-form via a new `hint_ids: &[&str]` field on `CommandNode` mirroring `usage_ids`** (revised in Phase B): a per-*node* key proved too coarse — `add`/`drop`/`show`/`create` are each one node spanning many forms, and a live-input hint for `add 1:n relationship` must be specific to relationships; `hint_key_for_input_in_mode` reuses `usage_key_for_input_in_mode`'s form-word disambiguation, and covers the advanced-SQL forms whose `usage_ids` are empty. Not keyed off `help_id` (it is `None` on the advanced-SQL nodes purely to dedup the `help` list; that parallel gap is issue **#36**). **Clause-concept hints** (`on delete` actions, constraint slots, `with pk`, cardinality) are a recorded **deferred extension** (`hint.concept.<topic>`, issue **#37**) — per-form is the right tier-3 granularity, with position-awareness owned by tier-2 + the live `Next:` line. Runtime `translate_error` classes resolve via stored `last_error_hint_key` (`hint` command / empty-F1). (The second route — pre-submit `diagnostic.*` read live from the walker on the F1 path — is **deferred**, issue **#38**: `Diagnostic` carries no class key.) Adds `AppCommand::Hint`, a `HINT` grammar node + REGISTRY entry, the `hint_ids` field, and `last_error_hint_key`; F1 is a read-only overlay (buffer + completion memo untouched). **Content is the bulk of the work** (the mechanism is ~a day): v1 scope = ~37 command forms + 9 runtime error classes (comprehensive for those, ~57 blocks), authored **exemplars-first** (voice approved in this ADR's `/runda` review, then mass-authored in batches), enforced by a **comprehensiveness coverage test**, with graceful fall-back to tier-2 if a key is ever missing. The **pre-submit-diagnostic route + ~33 `diagnostic.*` blocks were deferred** (issue **#38**) — `Diagnostic` carries no class key, so the route needs a broad change for marginal value (tier-2 already surfaces diagnostics; many duplicate runtime classes). Forks user-chosen: two-surface model; **F1** (vs `?` / a chord); no-arg; comprehensive-for-commands-and-errors scope; exemplars-first; diagnostics deferred. OOS: per-topic `hint <topic>` (rejected — overlaps `help`); always-on tier-3 (rejected — keeps ambient terse); non-`en-US` locales + success-command teaching (deferred); clause-concept hints (issue #37); the diagnostic route (issue #38); the `help`-side advanced-SQL gap (issue #36)
- [ADR-0054 — Release versioning policy + version surfaces (`--version` / `version`)](0054-release-versioning-and-version-surfaces.md) — **Accepted + implemented 2026-06-16** (plan: `docs/plans/20260616-public-availability.md`, step 1 on the road to public availability; no prior issue/`requirements.md` item — an untracked gap). Fixes the **tag↔crate-version decoupling**: `Cargo.toml` built `0.1.0` while `release.yaml` named assets from the git tag, so a binary could report a version different from the asset it shipped in. **Decision:** `Cargo.toml` `version` is the **single source of truth** (read via `env!("CARGO_PKG_VERSION")`, no tag-injection); two surfaces report it through one `cli::version_text()` → catalog `cli.version_line` — a **`--version` / `-V`** CLI flag (mirrors `--help`, prints+exits in `main.rs`) and an in-app **`version`** command (REGISTRY node `app::VERSION`, `AppCommand::Version`, emits via `note_system`); and a **release-CI version guard** (`release.yaml` `test` job parses `cargo metadata` and **fails the release** unless the `v*` tag equals `v<CARGO_PKG_VERSION>`). Release ritual: bump `Cargo.toml` → commit → tag → push. New keys `cli.version_line` + `help.app.version` + `parse.usage.version` + `hint.cmd.version.{what,example}` (the new REGISTRY command pulls in the comprehensiveness coverage gate). Rejected: tag-as-source (makes Cargo.toml lie). Deferred: git-hash/build-date enrichment (behind the same `version_text()` seam); UI placement beyond the command. Tested test-first: CLI parse (`--version`/`-V`/default-off), `version_text()` carries `CARGO_PKG_VERSION`, the in-app command parses + emits. Also corrected a stale `release.yaml` header comment ("macOS is deferred" → built by the dispatched `release-macos.yaml`).
- [ADR-0055 — `curl | sh` install script (`scripts/install.sh`)](0055-curl-sh-install-script.md) — **Accepted + implemented 2026-06-17** (plan: `docs/plans/20260616-public-availability.md`, step 2; tracked by plan + ADR, no Gitea issue — user decision). A one-line installer (`curl -fsSL <gitea-raw>/scripts/install.sh | sh`) so beginners don't hand-pick an asset + `chmod +x`. **POSIX `sh`** (shellcheck-clean), detects `uname` OS/arch → target triple (**Linux → the fully-static `*-musl`** build, macOS → `*-apple-darwin`; `amd64`/`arm64` aliased; **Windows rejected** → Scoop/winget/releases page), resolves the version from the **`releases/latest`** API (or `RDBMS_VERSION` to pin), downloads the asset **and its `.sha256` and verifies it** (mismatch aborts), installs to `~/.local/bin` (`RDBMS_INSTALL_DIR` override) with a PATH hint. Testing seams: `RDBMS_OS`/`RDBMS_ARCH` + `--print-target`. macOS note: `curl` downloads aren't Gatekeeper-quarantined so the ad-hoc binary runs as-is (Developer-ID + notarization is the postponed signing task). **Verified end-to-end against the live public `v0.1.0`** (all platform mappings, pinned + latest, checksum incl. tamper-rejection, install + run). Rejected: website-domain hosting (extra moving part; Gitea raw is simplest); deferred: `install.ps1`, uploading the script as a release asset, and a **shellcheck CI gate** (shellcheck isn't in the flake — touches ADR-ci-002).
+63 -28
View File
@@ -32,7 +32,20 @@ anything user-facing (taps, buckets).
---
## 2. `install.sh` (curl | sh) — DECIDED shape
## 2. `install.sh` (curl | sh) — DONE 2026-06-17 (ADR-0055)
**Shipped** `scripts/install.sh` (POSIX sh, shellcheck-clean). Verified
end-to-end against the live public `v0.1.0` release: platform mappings
(Linux/macOS × x86_64/aarch64; Windows + unknown arch error cleanly),
pinned (`RDBMS_VERSION`) and latest (`releases/latest`) paths, SHA-256
verification (incl. a tamper-rejection check), install to
`~/.local/bin`, PATH hint. **`install.ps1` (Windows) deferred** — Windows
users go via Scoop/winget (§3). The website copy that references the
`curl` command is the **website branch's** job (separate agent), later.
A **shellcheck CI gate** for `scripts/` is a recommended follow-up (not
added — shellcheck isn't in the flake yet; touches ADR-ci-002).
Original decided shape (for reference):
- **Hosted from the Gitea repo URL** on `git.lazyeval.net` (simplest):
`curl -fsSL https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh | sh`
@@ -72,11 +85,13 @@ per-release step to bump it. Ordered cheapest → most gatekept.
`cargo install cargo-binstall`). **Our instructions must say this.**
- Add `[package.metadata.binstall]` to `Cargo.toml` (pkg-url template →
our Gitea release assets; our naming already fits).
- **OPEN:** the frictionless `cargo binstall rdbms-playground` resolves
the crate via **crates.io**. Decision needed: **publish to crates.io?**
If not, document the `cargo binstall --git <gitea-url>` form instead.
*(Verify current cargo-binstall non-crates.io behaviour before
committing wording.)*
- **DECIDED (2026-06-17): publish to crates.io** — so the frictionless
`cargo binstall rdbms-playground` resolves the crate, and the project
is discoverable there. (A crates.io publish is its own small task:
metadata completeness — description/license/repository/keywords/readme
— and `cargo publish`; the `[package.metadata.binstall]` URL template
points binstall at our Gitea release assets.) *(Verify current
cargo-binstall behaviour when wiring.)*
### 3b. Scoop (Windows)
- A **bucket** repo under `lazyeval` on Gitea with a JSON manifest
@@ -113,29 +128,49 @@ per-release step to bump it. Ordered cheapest → most gatekept.
---
## Open decisions (need the user)
## Open decisions
1. **crates.io:** publish the crate? (changes the `cargo binstall` +
discoverability story).
2. **macOS signing — CONFIRMED BUG (2026-06-16).** `release-macos.yaml`
line 52 runs `codesign --force --sign -` (the `-` = **ad-hoc**), so a
downloaded binary is *not* properly signed — user-verified. The Apple
**Developer ID** identity was provisioned in the runner VM's keychain
(the CI agent asked for it) but the workflow never references it, so
it sits **unused**. Fix = a `release-macos.yaml` change: sign with
`codesign --sign "Developer ID Application: <Lazy Evaluation Ltd …>"`
against the keychain identity, **after** the `install_name_tool`
de-nix (which invalidates any signature), then ideally **notarize +
staple** (needs Apple notarytool creds — Apple ID + app-specific
password, or an App Store Connect API key — as runner secrets). A
distinct task from the version work; corrects the docs once landed.
3. **Issue tracking:** file Gitea issues for these (version; install.sh;
D3), or track via this plan + ADR only?
1. **crates.io:** **RESOLVED 2026-06-17 — yes, publish.** (See §3a.)
2. **Tracking:** **RESOLVED 2026-06-17 — doc + ADR only, no Gitea
issues.**
3. **Release downloads public:** **CONFIRMED 2026-06-17** — the Gitea
releases are publicly downloadable (no auth); `install.sh` relies on
it and was verified against the live `v0.1.0`.
### Still open / postponed
- **macOS signing — CONFIRMED BUG (2026-06-16), POSTPONED by the user
(2026-06-17)** pending the correct signing ID. Details:
- `release-macos.yaml` does `codesign --force --sign -` (ad-hoc) and has
**no signing scaffolding at all** (no keychain import, no secrets)
so a downloaded binary is *not* properly signed (user-verified).
- **The credential the user has is the wrong type:** `Apple Development:
Oliver Sturm (W687M898E4)` is a *development* cert (Gatekeeper won't
trust it for distribution). Distribution needs a **`Developer ID
Application`** cert (same format, different type). Signing under the
company name *"Lazy Evaluation Ltd"* would need an **Organization**
Apple Developer account; a personal account signs as "Oliver Sturm".
- **Notarization** (required with Developer ID for non-quarantined trust
on browser downloads): after signing, `xcrun notarytool submit`. Creds
= an **App Store Connect API key** (Issuer ID + Key ID + `.p8`,
recommended for CI) *or* Apple ID + app-specific password + Team ID.
A bare CLI binary can't be *stapled* (only bundles/dmg/pkg) — Gatekeeper
does an online check instead.
- **Urgency caveat:** the `curl|sh` path doesn't need any of this (curl
downloads aren't quarantined); signing matters for browser downloads
from the releases page. Fix when the right cert + creds exist; corrects
the ad-hoc docs once landed.
## Sequencing
1. **Version discipline** (ADR-0054) — `--version`/`-V` + `version`
command + CI tag-match guard + tests. **← building now.**
2. `scripts/install.sh` + confirm public download URLs.
3. Package managers, cheapest first: `cargo binstall` + Scoop → Homebrew
→ winget.
1. ✅ **Version discipline** (ADR-0054) — `--version`/`-V` + `version`
command + CI tag-match guard + tests.
2. ✅ **`scripts/install.sh`** (ADR-0055) — built + verified against the
live public release.
3. **← next:** package managers, cheapest first: `cargo binstall`
(+ crates.io publish) + Scoop → Homebrew (`lazyeval` tap) → winget
(komac / manual). Two `lazyeval` repos (tap + bucket) + CI push creds
to set up.
4. **Cut a release at a new version** — bump `Cargo.toml` (0.1.0 →
0.1.1/0.2.0; the ADR-0054 guard checks the tag), tag, push; the four
Linux/Windows targets build immediately. (macOS leg awaits signing.)
+166
View File
@@ -0,0 +1,166 @@
#!/bin/sh
# install.sh — download and install a prebuilt rdbms-playground binary.
#
# Quick start (Linux / macOS):
# curl -fsSL https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh | sh
#
# What it does: detects your OS + CPU, downloads the matching release binary
# from the Gitea releases (Linux uses the fully-static musl build), verifies
# its SHA-256 checksum, and installs it to ~/.local/bin.
#
# Environment overrides:
# RDBMS_VERSION install a specific tag (e.g. v0.2.0) instead of the latest
# RDBMS_INSTALL_DIR install directory (default: $HOME/.local/bin)
# RDBMS_OS force the OS (testing): Linux | Darwin
# RDBMS_ARCH force the CPU (testing): x86_64 | aarch64
#
# Flags:
# --print-target print the resolved target triple and exit (no download)
# -h, --help print this help and exit
#
# Notes:
# * Windows is not installable via this script (the binary is a .exe) —
# use Scoop/winget (planned) or download the .exe from the releases page.
# * macOS: a curl download is not quarantined by Gatekeeper, so the binary
# runs without extra steps. (Developer-ID signing + notarization is a
# separate, planned improvement for browser downloads.)
#
# POSIX sh — no bashisms, so it runs under the `sh` of `curl | sh`.
set -eu
REPO_BASE="https://git.lazyeval.net/oli/rdbms-playground"
API_BASE="https://git.lazyeval.net/api/v1/repos/oli/rdbms-playground"
BIN_NAME="rdbms-playground"
PRINT_TARGET=0
err() {
printf 'install: %s\n' "$1" >&2
exit 1
}
info() { printf '%s\n' "$1" >&2; }
usage() {
# Lines 2..(first blank) of this file are the human-readable header.
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
}
# Resolve the Rust target triple for the current (or forced) platform.
# Linux -> <arch>-unknown-linux-musl (the fully-static build)
# macOS -> <arch>-apple-darwin
detect_target() {
os="${RDBMS_OS:-$(uname -s)}"
arch="${RDBMS_ARCH:-$(uname -m)}"
case "$os" in
Linux | linux) os_part="unknown-linux-musl" ;;
Darwin | darwin | macos | macOS) os_part="apple-darwin" ;;
MINGW* | MSYS* | CYGWIN* | *Windows* | *windows*)
err "Windows is not supported by this installer — use Scoop/winget (planned) or download the .exe from $REPO_BASE/releases" ;;
*) err "unsupported operating system: $os" ;;
esac
case "$arch" in
x86_64 | amd64) arch_part="x86_64" ;;
aarch64 | arm64) arch_part="aarch64" ;;
*) err "unsupported CPU architecture: $arch" ;;
esac
printf '%s-%s' "$arch_part" "$os_part"
}
# Download $1 to file $2 (curl or wget).
download() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$1" -o "$2"
elif command -v wget >/dev/null 2>&1; then
wget -qO "$2" "$1"
else
err "need either curl or wget on PATH"
fi
}
# Fetch $1 to stdout (curl or wget).
fetch() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$1"
elif command -v wget >/dev/null 2>&1; then
wget -qO- "$1"
else
err "need either curl or wget on PATH"
fi
}
# The release tag to install: $RDBMS_VERSION if set, else the latest release.
resolve_version() {
if [ -n "${RDBMS_VERSION:-}" ]; then
printf '%s' "$RDBMS_VERSION"
return
fi
json=$(fetch "$API_BASE/releases/latest") ||
err "could not query the latest release from $API_BASE"
# Portable JSON scrape (no jq): the latest-release object carries exactly
# one "tag_name": "<tag>" field.
tag=$(printf '%s' "$json" |
grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' |
head -1 | sed 's/.*"\([^"]*\)"$/\1/')
[ -n "$tag" ] || err "could not parse the latest release tag"
printf '%s' "$tag"
}
# Verify file $1 against sha256 sidecar $2 (format: "<hash> <name>").
verify_checksum() {
expected=$(awk '{print $1; exit}' "$2")
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$1" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
actual=$(shasum -a 256 "$1" | awk '{print $1}')
else
info "warning: no sha256 tool found — skipping checksum verification"
return 0
fi
[ "$expected" = "$actual" ] ||
err "checksum mismatch (expected $expected, got $actual) — refusing to install"
}
main() {
while [ $# -gt 0 ]; do
case "$1" in
--print-target) PRINT_TARGET=1 ;;
-h | --help)
usage
exit 0
;;
*) err "unknown argument: $1 (try --help)" ;;
esac
shift
done
target=$(detect_target)
if [ "$PRINT_TARGET" = "1" ]; then
printf '%s\n' "$target"
exit 0
fi
version=$(resolve_version)
asset="$BIN_NAME-$version-$target"
url="$REPO_BASE/releases/download/$version/$asset"
dir="${RDBMS_INSTALL_DIR:-$HOME/.local/bin}"
tmp=$(mktemp -d 2>/dev/null) || err "could not create a temporary directory"
trap 'rm -rf "$tmp"' EXIT INT TERM
info "downloading $asset ..."
download "$url" "$tmp/$BIN_NAME" || err "download failed: $url"
download "$url.sha256" "$tmp/$BIN_NAME.sha256" || err "checksum download failed: $url.sha256"
verify_checksum "$tmp/$BIN_NAME" "$tmp/$BIN_NAME.sha256"
mkdir -p "$dir" || err "could not create install directory: $dir"
chmod +x "$tmp/$BIN_NAME"
mv "$tmp/$BIN_NAME" "$dir/$BIN_NAME" || err "could not install to $dir"
info "installed $BIN_NAME $version -> $dir/$BIN_NAME"
case ":${PATH:-}:" in
*":$dir:"*) ;;
*) info "note: $dir is not on your PATH. Add it, e.g.: export PATH=\"$dir:\$PATH\"" ;;
esac
}
main "$@"