diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml
index 45c655e..7e6a1db 100644
--- a/.github/workflows/release-binaries.yml
+++ b/.github/workflows/release-binaries.yml
@@ -3,8 +3,7 @@ name: Build and Publish Release
on:
push:
tags:
- - "v*"
- workflow_dispatch:
+ - "v*.*.*"
permissions:
contents: write
@@ -63,6 +62,8 @@ jobs:
if [ "$TAG" != "$VERSION" ]; then
echo "Tag does not match pyproject.toml"
+ echo "Tag: $TAG"
+ echo "Version: $VERSION"
exit 1
fi
@@ -76,27 +77,70 @@ jobs:
- name: Build PyInstaller binary
run: python scripts/build_pyinstaller.py
- # Generate checksum
- - name: Generate checksum
+ - name: Generate binary checksum
shell: bash
run: |
- cd node_version/dist/native/${{ matrix.target }}
- for f in explainthisrepo*; do
- if command -v sha256sum >/dev/null 2>&1; then
- sha256sum "$f" > "$f.sha256"
+ set -euo pipefail
+
+ TARGET="${{ matrix.target }}"
+ DIR="node_version/dist/native/${TARGET}"
+
+ if [[ "${TARGET}" == win-* ]]; then
+ BINARY="explainthisrepo.exe"
+ else
+ BINARY="explainthisrepo"
+ fi
+
+ test -f "${DIR}/${BINARY}" || {
+ echo "Expected binary not found: ${DIR}/${BINARY}"
+ exit 1
+ }
+
+ cd "${DIR}"
+
+ if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "${BINARY}" > "${BINARY}.sha256"
+ else
+ shasum -a 256 "${BINARY}" > "${BINARY}.sha256"
+ fi
+
+ - name: Verify binary checksum
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ TARGET="${{ matrix.target }}"
+ DIR="node_version/dist/native/${TARGET}"
+
+ cd "${DIR}"
+
+ if command -v sha256sum >/dev/null 2>&1; then
+ if [[ "${TARGET}" == win-* ]]; then
+ sha256sum -c explainthisrepo.exe.sha256
else
- shasum -a 256 "$f" > "$f.sha256"
+ sha256sum -c explainthisrepo.sha256
+ fi
+ else
+ if [[ "${TARGET}" == win-* ]]; then
+ EXPECTED="$(awk '{print $1}' explainthisrepo.exe.sha256)"
+ ACTUAL="$(shasum -a 256 explainthisrepo.exe | awk '{print $1}')"
+ else
+ EXPECTED="$(awk '{print $1}' explainthisrepo.sha256)"
+ ACTUAL="$(shasum -a 256 explainthisrepo | awk '{print $1}')"
fi
- done
+
+ test "${EXPECTED}" = "${ACTUAL}"
+ fi
- name: Upload native binary + checksum
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: node_version/dist/native/${{ matrix.target }}
+ if-no-files-found: error
release:
- name: Publish npm and GitHub Release
+ name: Package and Publish Release
needs: build
runs-on: ubuntu-latest
@@ -112,13 +156,19 @@ jobs:
- name: Extract canonical version
shell: bash
run: |
+ set -euo pipefail
+
mkdir -p .ci
+
VERSION="$(python scripts/get_version.py)"
+
printf '%s\n' "$VERSION" > .ci/version.txt
- name: Validate git tag matches pyproject version
shell: bash
run: |
+ set -euo pipefail
+
if [[ "${GITHUB_REF}" != refs/tags/* ]]; then
echo "This workflow must run from a git tag"
exit 1
@@ -129,12 +179,16 @@ jobs:
if [ "$TAG" != "$VERSION" ]; then
echo "Tag does not match pyproject.toml"
+ echo "Tag: $TAG"
+ echo "Version: $VERSION"
exit 1
fi
- name: Rewrite manifests from canonical version
shell: bash
run: |
+ set -euo pipefail
+
python - <<'PY'
import json
import re
@@ -146,6 +200,7 @@ jobs:
package_json_path = Path("node_version/package.json")
package_json = json.loads(package_json_path.read_text(encoding="utf-8"))
package_json["version"] = version
+
package_json_path.write_text(
json.dumps(package_json, indent=2) + "\n",
encoding="utf-8",
@@ -153,13 +208,19 @@ jobs:
# node_version/package-lock.json
package_lock_path = Path("node_version/package-lock.json")
+
if package_lock_path.exists():
- package_lock = json.loads(package_lock_path.read_text(encoding="utf-8"))
+ package_lock = json.loads(
+ package_lock_path.read_text(encoding="utf-8")
+ )
+
package_lock["version"] = version
packages = package_lock.get("packages")
+
if isinstance(packages, dict):
root_pkg = packages.get("")
+
if isinstance(root_pkg, dict):
root_pkg["version"] = version
@@ -171,6 +232,7 @@ jobs:
# dotnet_version/ExplainThisRepo.csproj
csproj_path = Path("dotnet_version/ExplainThisRepo.csproj")
csproj_text = csproj_path.read_text(encoding="utf-8")
+
new_text, count = re.subn(
r".*?",
f"{version}",
@@ -178,8 +240,12 @@ jobs:
count=1,
flags=re.S,
)
+
if count != 1:
- raise SystemExit("Could not update in ExplainThisRepo.csproj")
+ raise SystemExit(
+ "Could not update in ExplainThisRepo.csproj"
+ )
+
csproj_path.write_text(new_text, encoding="utf-8")
# Generated version files
@@ -189,7 +255,10 @@ jobs:
Path("node_version/_version.py"),
]:
if version_file.exists():
- version_file.write_text(f'VERSION = "{version}"\n', encoding="utf-8")
+ version_file.write_text(
+ f'VERSION = "{version}"\n',
+ encoding="utf-8",
+ )
PY
- name: Download native binaries
@@ -197,72 +266,333 @@ jobs:
with:
path: artifacts
- - name: Rehydrate native folder
+ - name: Rehydrate native folders and human-readable release assets
shell: bash
run: |
- mkdir -p node_version/dist/native dotnet_version/native release
+ set -euo pipefail
+
+ VERSION="$(cat .ci/version.txt)"
+
+ mkdir -p \
+ node_version/dist/native \
+ dotnet_version/native \
+ release
for artifact in artifacts/*; do
[ -d "$artifact" ] || continue
- target="$(basename "$artifact")"
-
- mkdir -p "node_version/dist/native/$target"
- mkdir -p "dotnet_version/native/$target"
- cp "$artifact"/explainthisrepo* "node_version/dist/native/$target/"
- cp "$artifact"/explainthisrepo* "dotnet_version/native/$target/"
+ TARGET="$(basename "$artifact")"
- case "$target" in
+ case "$TARGET" in
darwin-arm64)
- release_name="ExplainThisRepo-macOS-Apple-Silicon-arm64"
- binary="explainthisrepo"
+ RELEASE_NAME="ExplainThisRepo-macOS-Apple-Silicon-arm64"
+ BINARY="explainthisrepo"
;;
+
darwin-x64)
- release_name="ExplainThisRepo-macOS-Intel-x64"
- binary="explainthisrepo"
+ RELEASE_NAME="ExplainThisRepo-macOS-Intel-x64"
+ BINARY="explainthisrepo"
;;
+
linux-arm64)
- release_name="ExplainThisRepo-Linux-ARM64"
- binary="explainthisrepo"
+ RELEASE_NAME="ExplainThisRepo-Linux-ARM64"
+ BINARY="explainthisrepo"
;;
+
linux-x64)
- release_name="ExplainThisRepo-Linux-x64"
- binary="explainthisrepo"
+ RELEASE_NAME="ExplainThisRepo-Linux-x64"
+ BINARY="explainthisrepo"
;;
+
win-arm64)
- release_name="ExplainThisRepo-Windows-ARM64.exe"
- binary="explainthisrepo.exe"
+ RELEASE_NAME="ExplainThisRepo-Windows-ARM64.exe"
+ BINARY="explainthisrepo.exe"
;;
+
win-x64)
- release_name="ExplainThisRepo-Windows-x64.exe"
- binary="explainthisrepo.exe"
+ RELEASE_NAME="ExplainThisRepo-Windows-x64.exe"
+ BINARY="explainthisrepo.exe"
;;
+
*)
- echo "Unknown target: $target"
+ echo "Unknown target: $TARGET"
exit 1
;;
esac
- cp "$artifact/$binary" "release/$release_name"
- cp "$artifact/$binary.sha256" "release/$release_name.sha256"
- done
+ test -f "$artifact/$BINARY" || {
+ echo "Missing binary: $artifact/$BINARY"
+ exit 1
+ }
+
+ test -f "$artifact/$BINARY.sha256" || {
+ echo "Missing checksum: $artifact/$BINARY.sha256"
+ exit 1
+ }
+
+ mkdir -p "node_version/dist/native/$TARGET"
+ mkdir -p "dotnet_version/native/$TARGET"
+
+ cp "$artifact/$BINARY" \
+ "node_version/dist/native/$TARGET/$BINARY"
+
+ cp "$artifact/$BINARY.sha256" \
+ "node_version/dist/native/$TARGET/$BINARY.sha256"
+
+ cp "$artifact/$BINARY" \
+ "dotnet_version/native/$TARGET/$BINARY"
+
+ cp "$artifact/$BINARY.sha256" \
+ "dotnet_version/native/$TARGET/$BINARY.sha256"
- # HARD STOP if tampered
- - name: Verify checksums
+ cp "$artifact/$BINARY" \
+ "release/$RELEASE_NAME"
+
+ cp "$artifact/$BINARY.sha256" \
+ "release/$RELEASE_NAME.sha256"
+ done
+
+ - name: Verify all native binary checksums
shell: bash
run: |
- for checksum in $(find node_version/dist/native -name '*.sha256' | sort); do
- dir="$(dirname "$checksum")"
+ set -euo pipefail
+
+ for target_dir in node_version/dist/native/*; do
+ [ -d "$target_dir" ] || continue
+
+ TARGET="$(basename "$target_dir")"
+
+ if [[ "${TARGET}" == win-* ]]; then
+ BINARY="explainthisrepo.exe"
+ else
+ BINARY="explainthisrepo"
+ fi
+
+ test -f "${target_dir}/${BINARY}"
+ test -f "${target_dir}/${BINARY}.sha256"
+
+ (
+ cd "${target_dir}"
+ sha256sum -c "${BINARY}.sha256"
+ )
+ done
+
+ - name: Verify exactly six native targets exist
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ EXPECTED_TARGETS="
+ darwin-arm64
+ darwin-x64
+ linux-arm64
+ linux-x64
+ win-arm64
+ win-x64
+ "
+
+ for target in $EXPECTED_TARGETS; do
+ test -f "node_version/dist/native/${target}/explainthisrepo" \
+ || test -f "node_version/dist/native/${target}/explainthisrepo.exe" \
+ || {
+ echo "Missing native target: ${target}"
+ exit 1
+ }
+ done
+
+ - name: Package CLI installer archives
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ VERSION="$(cat .ci/version.txt)"
+
+ package_unix() {
+ TARGET="$1"
+ BINARY="$2"
+
+ SOURCE_DIR="node_version/dist/native/${TARGET}"
+ ARCHIVE="explainthisrepo-v${VERSION}-cli-installer-${TARGET}.tar.gz"
+
+ STAGING="$(mktemp -d)"
+
+ cleanup_staging() {
+ rm -rf "${STAGING}"
+ }
+
+ trap cleanup_staging RETURN
+
+ cp "${SOURCE_DIR}/${BINARY}" "${STAGING}/${BINARY}"
+
+ (
+ cd "${STAGING}"
+ tar -czf "${GITHUB_WORKSPACE}/release/${ARCHIVE}" "${BINARY}"
+ )
+
+ sha256sum \
+ "release/${ARCHIVE}" \
+ > "release/${ARCHIVE}.sha256"
+ }
+
+ package_windows() {
+ TARGET="$1"
+ BINARY="explainthisrepo.exe"
+
+ SOURCE_DIR="node_version/dist/native/${TARGET}"
+ ARCHIVE="explainthisrepo-v${VERSION}-cli-installer-${TARGET}.zip"
+
+ STAGING="$(mktemp -d)"
+
+ cleanup_staging() {
+ rm -rf "${STAGING}"
+ }
+
+ trap cleanup_staging RETURN
+
+ cp "${SOURCE_DIR}/${BINARY}" "${STAGING}/${BINARY}"
+
(
- cd "$dir"
- sha256sum -c "$(basename "$checksum")"
+ cd "${STAGING}"
+ zip -q "${GITHUB_WORKSPACE}/release/${ARCHIVE}" "${BINARY}"
)
+
+ sha256sum \
+ "release/${ARCHIVE}" \
+ > "release/${ARCHIVE}.sha256"
+ }
+
+ package_unix "linux-x64" "explainthisrepo"
+ package_unix "linux-arm64" "explainthisrepo"
+ package_unix "darwin-x64" "explainthisrepo"
+ package_unix "darwin-arm64" "explainthisrepo"
+
+ package_windows "win-x64"
+ package_windows "win-arm64"
+
+ - name: Verify CLI installer archives
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ VERSION="$(cat .ci/version.txt)"
+
+ for archive in release/*-cli-installer-*.tar.gz; do
+ test -f "${archive}"
+ sha256sum -c "${archive}.sha256"
done
- - name: Verify native binaries exist
+ for archive in release/*-cli-installer-*.zip; do
+ test -f "${archive}"
+ sha256sum -c "${archive}.sha256"
+ done
+
+ - name: Verify archive contents
shell: bash
run: |
- find node_version/dist/native -type f | sort
+ set -euo pipefail
+
+ VERSION="$(cat .ci/version.txt)"
+
+ verify_tar() {
+ ARCHIVE="$1"
+
+ CONTENTS="$(tar -tzf "${ARCHIVE}")"
+
+ EXPECTED="explainthisrepo"
+
+ if [ "${CONTENTS}" != "${EXPECTED}" ]; then
+ echo "Invalid archive contents: ${ARCHIVE}"
+ echo "Expected:"
+ echo "${EXPECTED}"
+ echo "Actual:"
+ echo "${CONTENTS}"
+ exit 1
+ fi
+ }
+
+ verify_zip() {
+ ARCHIVE="$1"
+
+ CONTENTS="$(unzip -Z1 "${ARCHIVE}")"
+
+ EXPECTED="explainthisrepo.exe"
+
+ if [ "${CONTENTS}" != "${EXPECTED}" ]; then
+ echo "Invalid archive contents: ${ARCHIVE}"
+ echo "Expected:"
+ echo "${EXPECTED}"
+ echo "Actual:"
+ echo "${CONTENTS}"
+ exit 1
+ fi
+ }
+
+ verify_tar \
+ "release/explainthisrepo-v${VERSION}-cli-installer-linux-x64.tar.gz"
+
+ verify_tar \
+ "release/explainthisrepo-v${VERSION}-cli-installer-linux-arm64.tar.gz"
+
+ verify_tar \
+ "release/explainthisrepo-v${VERSION}-cli-installer-darwin-x64.tar.gz"
+
+ verify_tar \
+ "release/explainthisrepo-v${VERSION}-cli-installer-darwin-arm64.tar.gz"
+
+ verify_zip \
+ "release/explainthisrepo-v${VERSION}-cli-installer-win-x64.zip"
+
+ verify_zip \
+ "release/explainthisrepo-v${VERSION}-cli-installer-win-arm64.zip"
+
+ - name: Verify release asset layout
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ VERSION="$(cat .ci/version.txt)"
+
+ expected_assets="
+ ExplainThisRepo-Linux-x64
+ ExplainThisRepo-Linux-x64.sha256
+ ExplainThisRepo-Linux-ARM64
+ ExplainThisRepo-Linux-ARM64.sha256
+ ExplainThisRepo-macOS-Apple-Silicon-arm64
+ ExplainThisRepo-macOS-Apple-Silicon-arm64.sha256
+ ExplainThisRepo-macOS-Intel-x64
+ ExplainThisRepo-macOS-Intel-x64.sha256
+ ExplainThisRepo-Windows-ARM64.exe
+ ExplainThisRepo-Windows-ARM64.exe.sha256
+ ExplainThisRepo-Windows-x64.exe
+ ExplainThisRepo-Windows-x64.exe.sha256
+ explainthisrepo-v${VERSION}-cli-installer-linux-x64.tar.gz
+ explainthisrepo-v${VERSION}-cli-installer-linux-x64.tar.gz.sha256
+ explainthisrepo-v${VERSION}-cli-installer-linux-arm64.tar.gz
+ explainthisrepo-v${VERSION}-cli-installer-linux-arm64.tar.gz.sha256
+ explainthisrepo-v${VERSION}-cli-installer-darwin-arm64.tar.gz
+ explainthisrepo-v${VERSION}-cli-installer-darwin-arm64.tar.gz.sha256
+ explainthisrepo-v${VERSION}-cli-installer-darwin-x64.tar.gz
+ explainthisrepo-v${VERSION}-cli-installer-darwin-x64.tar.gz.sha256
+ explainthisrepo-v${VERSION}-cli-installer-win-x64.zip
+ explainthisrepo-v${VERSION}-cli-installer-win-x64.zip.sha256
+ explainthisrepo-v${VERSION}-cli-installer-win-arm64.zip
+ explainthisrepo-v${VERSION}-cli-installer-win-arm64.zip.sha256
+ "
+
+ for asset in $expected_assets; do
+ test -f "release/${asset}" || {
+ echo "Missing release asset: ${asset}"
+ exit 1
+ }
+ done
+
+ ASSET_COUNT="$(find release -maxdepth 1 -type f | wc -l | tr -d ' ')"
+
+ if [ "${ASSET_COUNT}" != "24" ]; then
+ echo "Expected 24 release assets, found ${ASSET_COUNT}"
+ find release -maxdepth 1 -type f -printf '%f\n' | sort
+ exit 1
+ fi
- name: Setup Node
uses: actions/setup-node@v4
@@ -276,7 +606,9 @@ jobs:
working-directory: node_version
shell: bash
run: |
- PACKAGE_NAME=$(node -p "require('./package.json').name")
+ set -euo pipefail
+
+ PACKAGE_NAME="$(node -p "require('./package.json').name")"
VERSION="$(cat ../.ci/version.txt)"
if npm view "$PACKAGE_NAME@$VERSION" version >/dev/null 2>&1; then
@@ -298,20 +630,23 @@ jobs:
- name: Verify package contains native binaries
working-directory: node_version
+ shell: bash
run: |
- TAR=$(ls *.tgz)
- tar -tf "$TAR" | grep "dist/native" || (echo "Missing native binaries in package" && exit 1)
+ set -euo pipefail
+
+ TAR="$(ls *.tgz)"
+
+ tar -tf "$TAR" |
+ grep "dist/native" ||
+ (
+ echo "Missing native binaries in package"
+ exit 1
+ )
- name: Package check
working-directory: node_version
run: npm pack --dry-run
- - name: Publish to npm
- working-directory: node_version
- env:
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm publish --access public
-
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
@@ -321,10 +656,14 @@ jobs:
working-directory: dotnet_version
shell: bash
run: |
- PACKAGE_ID=$(grep -oPm1 "(?<=)[^<]+" ExplainThisRepo.csproj)
+ set -euo pipefail
+
+ PACKAGE_ID="$(grep -oPm1 "(?<=)[^<]+" ExplainThisRepo.csproj)"
VERSION="$(cat ../.ci/version.txt)"
- if curl -fsSL "https://api.nuget.org/v3-flatcontainer/${PACKAGE_ID,,}/index.json" | grep "\"$VERSION\"" >/dev/null; then
+ if curl -fsSL \
+ "https://api.nuget.org/v3-flatcontainer/${PACKAGE_ID,,}/index.json" |
+ grep "\"$VERSION\"" >/dev/null; then
echo "Version already exists on NuGet"
exit 1
fi
@@ -333,14 +672,25 @@ jobs:
working-directory: dotnet_version
run: dotnet pack -c Release
+ - name: Publish GitHub Release
+ uses: softprops/action-gh-release@v3
+ with:
+ files: release/*
+ generate_release_notes: true
+ fail_on_unmatched_files: true
+
+ - name: Publish to npm
+ working-directory: node_version
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: npm publish --access public
+
- name: Publish to NuGet
working-directory: dotnet_version
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
- run: dotnet nuget push nupkg/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json
-
- - name: Publish GitHub Release
- uses: softprops/action-gh-release@v2
- with:
- files: release/*
- generate_release_notes: true
\ No newline at end of file
+ run: |
+ dotnet nuget push \
+ nupkg/*.nupkg \
+ -k "${NUGET_API_KEY}" \
+ -s https://api.nuget.org/v3/index.json
\ No newline at end of file
diff --git a/README.md b/README.md
index e425231..c699cfe 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,8 @@ ExplainThisRepo supports multiple installation methods:
- pip (Python package)
- npm (prebuilt native binaries)
- .NET global tool
-- standalone binaries
+- standalone install CLI
+- manual standalone binaries
### Quick install
@@ -71,6 +72,42 @@ npx explainthisrepo owner/repo
dotnet tool install -g ExplainThisRepo
```
+#### Standalone CLI Install
+
+Install ExplainThisRepo with one command:
+
+```bash
+curl -fsSL https://cli.explainthisrepo.com/install.sh | sh
+```
+
+The shell installer automatically detects your operating system and CPU architecture, downloads the correct native binary, verifies it, and installs `explainthisrepo` to `/usr/local/bin`.
+
+### Supported platforms
+
+| Operating system| Architecture
+|---------------- |---------------|
+| Linux| x64 |
+| Linux| ARM64 |
+| macOS| Intel |
+| macOS| Apple Silicon |
+| Windows| x64 |
+| Windows| ARM64 |
+
+The one-command shell installer currently supports Linux, macOS and Windows through with Git Bash/WSL. Windows (through Powershell or Commnd Prompt) users can download the appropriate Windows executable from the latest [release](https://github.com/calchiwo/ExplainThisRepo/releases/latest).
+
+You do not need Python, pip, Node.js, npm to use the standalone CLI installation.
+
+The installer downloads a prebuilt native ExplainThisRepo binary.
+
+#### Manual standalone binaries installation
+
+If you prefer to install manually, download the binary for your platform from the latest [GitHub release](https://github.com/calchiwo/ExplainThisRepo/releases/latest).
+
+For installation details, troubleshooting, and the distribution architecture, see:
+
+- [docs/INSTALLATION.md](docs/INSTALLATION.md)
+- [docs/RELEASE-DISTRIBUTION.md](docs/RELEASE-DISTRIBUTION.md)
+
#### Run
```bash
@@ -84,6 +121,9 @@ explainthisrepo owner/repo
explain-this-repo owner/repo
etr owner/repo
explain owner/repo
+ExplainThisRepo owner/repo
+explainthis owner/repo
+explain-this owner/repo
```
Replace `owner/repo` with the GitHub repository identifier (e.g., `facebook/react`, `torvalds/linux`).
diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md
index bdcacfc..75f7a25 100644
--- a/docs/INSTALLATION.md
+++ b/docs/INSTALLATION.md
@@ -113,6 +113,7 @@ ExplainThisRepo can be installed in multiple ways:
- `npm` for Node users
- `dotnet tool` for .NET users
- standalone binaries for direct download
+- standalone install CLI
All of them run the same core Python engine compiled into native binaries.
@@ -124,24 +125,287 @@ Prebuilt standalone binaries are available for macOS, Linux, and Windows.
Download the latest release: [ExplainThisRepo latest releases](https://github.com/calchiwo/ExplainThisRepo/releases/latest)
-Or install directly:
-macOS
+## Option 5: Install as a standalone native CLI.
+
+You do not need Python, pip, Node.js, npm, or any other runtime to use the standalone installation.
+
+### One-command installation
+
+ExplainThisRepo provides native CLI binaries for Linux, macOS and Windows across x64 and ARM64 targets.
+
+The one-command shell installer works on linux, macOS, and Windows through Git Bash or WSL.
+
+#### Linux, macOS and Windows (with Git Bash/WSL)
+
+The recommended installation method is:
```bash
-curl -L https://github.com/calchiwo/ExplainThisRepo/releases/latest/download/explainthisrepo-darwin-arm64 -o explainthisrepo
-chmod +x explainthisrepo
+curl -fsSL https://cli.explainthisrepo.com/install.sh | sh
+```
+
+or without the `https://` scheme:
+
+```bash
+curl -fsSL cli.explainthisrepo.com/install.sh | sh
+```
+
+The installer automatically:
+
+1. Detects the operating system.
+2. Detects the CPU architecture.
+3. Maps the machine to a supported target.
+4. Resolves the latest stable ExplainThisRepo release.
+5. Downloads the matching CLI installer archive.
+6. Downloads the archive SHA256 checksum.
+7. Verifies the archive before extracting it.
+8. Extracts the executable.
+9. Installs it to `/usr/local/bin`.
+10. Makes the executable available as `explainthisrepo`.
+
+After installation:
+
+```bash
+explainthisrepo
+```
+
+#### Windows with Powershell or Commnd Prompt
+
+The current `install.sh` is a Unix shell installer and therefore handles Linux, Windows (with Git Bash/WSL) and macOS.
+
+Powershell/CMD doesn't use the `install.sh` installer, it uses the published [Windows executable or ZIP archive](https://github.com/calchiwo/ExplainThisRepo/releases/latest).
+
+Windows users can download the appropriate Windows installer archive, for:
+
+- Windows x64
+- Windows ARM64
+
+from the GitHub Release for the version you want, extract `explainthisrepo.exe`, and place it somewhere on your `PATH`.
+
+### Where the binary is installed
+
+The default installation directory is:
+
+`/usr/local/bin`
+
+The resulting executable is:
+
+`/usr/local/bin/explainthisrepo`
+
+If `/usr/local/bin` requires administrator access, the installer uses `sudo`.
+
+The installer does not ask for sudo before it is necessary.
+
+### Custom installation directory
+
+The installer supports an environment variable for controlled installations and testing:
+
+```bash
+EXPLAINTHISREPO_INSTALL_DIR="$HOME/.local/bin" \
+curl -fsSL https://cli.explainthisrepo.com/install.sh | sh
+```
+
+Make sure the selected directory is in your `PATH`.
+
+For example:
+
+```bash
+export PATH="$HOME/.local/bin:$PATH"
+```
+
+The installer cannot modify the parent shell's environment, so a `PATH` change may need to be made separately.
+
+### What the installer downloads
+
+Each release contains a platform-specific CLI installer archive.
+
+`explainthisrepo-v-cli-installer-.`
+
+For example:
+
+`explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz`
+
+The archive contains exactly one executable:
+
+`explainthisrepo`
+
+There are no nested directories, README files, version files, or other installer files inside the archive.
+
+The checksum is distributed separately:
+
+`explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz.sha256`
+
+The installer verifies the archive before extracting it.
+
+### Manual installation
+
+If you do not want to use the installer, download the appropriate binary from the GitHub release.
+
+The release provides human-readable native binaries such as:
+
+```
+ExplainThisRepo-Linux-x64
+ExplainThisRepo-Linux-ARM64
+ExplainThisRepo-macOS-Apple-Silicon-arm64
+ExplainThisRepo-macOS-Intel-x64
+ExplainThisRepo-Windows-ARM64.exe
+ExplainThisRepo-Windows-x64.exe
```
-Linux
+For Linux and macOS, make the downloaded binary executable:
```bash
-curl -L https://github.com/calchiwo/ExplainThisRepo/releases/latest/download/explainthisrepo-linux-x64 -o explainthisrepo
chmod +x explainthisrepo
```
-Windows (PowerShell)
+Then place it somewhere in your `PATH`, for example:
+
+```bash
+sudo mv explainthisrepo /usr/local/bin/explainthisrepo
+```
+
+Verify:
-```powershell
-curl -L https://github.com/calchiwo/ExplainThisRepo/releases/latest/download/explainthisrepo-win-x64.exe -o explainthisrepo.exe
+```bash
+explainthisrepo
```
+
+### Updating
+
+The one-command installer always resolves the latest stable GitHub release.
+
+To update an existing installation, use:
+
+```bash
+curl -fsSL https://cli.explainthisrepo.com/install.sh | sh
+```
+
+It replaces the existing executable with the binary from the latest stable release.
+
+### Troubleshooting
+
+`Unsupported operating system`
+
+The current shell installer supports:
+
+- Linux
+- macOS
+- Windows (with Git Bash/WSL)
+
+Windows (with Powershell/CMD) is not handled by `install.sh`.
+
+`Unsupported architecture`
+
+The current shell installer supports:
+
+- x64
+- ARM64
+
+Other architectures are rejected rather than receiving an incorrect binary.
+
+`Could not determine the latest ExplainThisRepo release`
+
+The installer could not retrieve the latest release information from GitHub.
+
+Check that the machine has network access and try again.
+
+`Could not download ...`
+
+The requested installer archive does not appear to be available at the expected release URL.
+
+This normally indicates a release packaging problem rather than a local installation problem.
+
+The release must contain an archive matching the installer's naming convention.
+
+`SHA256 verification failed`
+
+Do not continue the installation.
+
+The installer stops before extracting the archive when its SHA256 checksum does not match.
+
+"sudo is not available"
+
+The default installation directory requires administrator access and "sudo" is not available.
+
+Use a user-owned installation directory instead:
+
+```bash
+EXPLAINTHISREPO_INSTALL_DIR="$HOME/.local/bin" \
+curl -fsSL https://cli.explainthisrepo.com/install.sh | sh
+```
+
+Then ensure that directory is in your "PATH".
+
+`explainthisrepo: command not found`
+
+First check whether the binary exists:
+
+```bash
+ls -l /usr/local/bin/explainthisrepo
+```
+
+Then check whether `/usr/local/bin` is in your `PATH`:
+
+```bash
+echo "$PATH"
+```
+
+If it is missing, add it to your shell's `PATH`.
+
+### Installation architecture
+
+The installation system is intentionally split into two layers.
+
+The release pipeline creates the native binaries:
+
+```
+PyInstaller
+ ↓
+six native binaries
+```
+
+The release pipeline then creates the installer archives:
+
+```
+native binary
+ ↓
+CLI installer archive
+ ↓
+GitHub Release
+```
+
+The installer consumes those archives:
+
+```
+curl | sh
+ ↓
+detect OS
+ ↓
+detect architecture
+ ↓
+target
+ ↓
+latest release
+ ↓
+download archive
+ ↓
+verify SHA256
+ ↓
+extract
+ ↓
+install
+```
+
+The installer does not build anything.
+
+It only selects and installs an already-built native executable.
+
+### Installation endpoint
+
+The public installation command is:
+
+`https://cli.explainthisrepo.com/install.sh`
+
+This URL is a stable distribution interface.
+
+The implementation behind the URL may change, but the public installation command should remain stable.
\ No newline at end of file
diff --git a/docs/RELEASE-DISTRIBUTION.md b/docs/RELEASE-DISTRIBUTION.md
new file mode 100644
index 0000000..b076a12
--- /dev/null
+++ b/docs/RELEASE-DISTRIBUTION.md
@@ -0,0 +1,712 @@
+# ExplainThisRepo Release Distribution
+
+This document describes how ExplainThisRepo moves from a version tag to installable native binaries.
+
+The distribution system has one primary goal:
+
+```
+git tag
+ ↓
+automated build
+ ↓
+validated native binaries
+ ↓
+installer archives
+ ↓
+GitHub Release
+ ↓
+curl | sh
+ ↓
+installed CLI
+```
+
+## 1. Distribution model
+
+ExplainThisRepo has one CLI and two public release artifact classes.
+
+Human-readable native binaries
+
+These are intended for people browsing a GitHub Release:
+
+```
+ExplainThisRepo-Linux-x64
+ExplainThisRepo-Linux-ARM64
+ExplainThisRepo-macOS-Apple-Silicon-arm64
+ExplainThisRepo-macOS-Intel-x64
+ExplainThisRepo-Windows-ARM64.exe
+ExplainThisRepo-Windows-x64.exe
+```
+
+These names are deliberately human-readable.
+
+CLI installer archives
+
+These are intended for automated installation:
+
+```
+explainthisrepo-v-cli-installer-linux-x64.tar.gz
+explainthisrepo-v-cli-installer-linux-arm64.tar.gz
+explainthisrepo-v-cli-installer-darwin-arm64.tar.gz
+explainthisrepo-v-cli-installer-darwin-x64.tar.gz
+explainthisrepo-v-cli-installer-win-x64.zip
+explainthisrepo-v-cli-installer-win-arm64.zip
+```
+
+The `cli-installer` component explicitly identifies the purpose of these artifacts.
+
+The two artifact classes are not replacements for each other.
+
+```
+CI target
+ │
+ ├── human-readable binary
+ │
+ └── CLI installer archive
+```
+
+## 2. Supported targets
+
+The build matrix currently contains six targets:
+
+```
+darwin-arm64
+darwin-x64
+linux-arm64
+linux-x64
+win-arm64
+win-x64
+```
+
+These target names are machine-oriented identifiers.
+
+They are used internally by:
+
+- GitHub Actions
+- PyInstaller
+- native binary directories
+- release packaging
+- installer routing
+
+The target names should remain stable because they are part of the distribution system's internal contract.
+
+## 3. Native build layer
+
+The native executable is produced by:
+
+`scripts/build_pyinstaller.py`
+
+The build system already knows how to build all six targets.
+
+This distribution layer does not modify the PyInstaller build logic.
+
+The build pipeline therefore remains:
+
+```
+source code
+ ↓
+PyInstaller
+ ↓
+native executable
+```
+
+Examples:
+
+```
+linux-x64
+└── explainthisrepo
+
+linux-arm64
+└── explainthisrepo
+
+darwin-x64
+└── explainthisrepo
+
+darwin-arm64
+└── explainthisrepo
+
+win-x64
+└── explainthisrepo.exe
+
+win-arm64
+└── explainthisrepo.exe
+```
+
+## 4. Checksums
+
+Every native binary receives a SHA256 checksum.
+
+For example:
+
+```
+explainthisrepo
+explainthisrepo.sha256
+```
+
+The release pipeline verifies these checksums before continuing.
+
+The installer archives receive their own checksums.
+
+For example:
+
+```
+explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz
+explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz.sha256
+```
+
+The archive checksum is the important checksum for installation because the installer downloads the archive.
+
+The installation integrity flow is:
+
+```
+download archive
+ ↓
+download archive checksum
+ ↓
+verify archive
+ ↓
+extract
+ ↓
+install
+```
+
+## 5. Installer archive contract
+
+Every CLI installer archive has a strict content contract.
+
+Linux and macOS archives contain:
+
+`explainthisrepo`
+
+Windows archives contain:
+
+`explainthisrepo.exe`
+
+There are no:
+
+- nested directories
+- README files
+- version files
+- metadata files
+- checksums inside the archive
+
+The checksum is a separate release asset.
+
+This makes extraction deterministic.
+
+The installer does not need to search the extracted directory for the executable.
+
+6. Release artifact layout
+
+A release such as `v0.29.0` contains both public artifact classes.
+
+```
+GitHub Release v0.29.0
+│
+├── Human-readable binaries
+│ ├── ExplainThisRepo-Linux-x64
+│ ├── ExplainThisRepo-Linux-x64.sha256
+│ ├── ExplainThisRepo-Linux-ARM64
+│ ├── ExplainThisRepo-Linux-ARM64.sha256
+│ ├── ExplainThisRepo-macOS-Apple-Silicon-arm64
+│ ├── ExplainThisRepo-macOS-Apple-Silicon-arm64.sha256
+│ ├── ExplainThisRepo-macOS-Intel-x64
+│ ├── ExplainThisRepo-macOS-Intel-x64.sha256
+│ ├── ExplainThisRepo-Windows-ARM64.exe
+│ ├── ExplainThisRepo-Windows-ARM64.exe.sha256
+│ ├── ExplainThisRepo-Windows-x64.exe
+│ └── ExplainThisRepo-Windows-x64.exe.sha256
+│
+└── CLI installer archives
+ ├── explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz
+ ├── explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz.sha256
+ ├── explainthisrepo-v0.29.0-cli-installer-linux-arm64.tar.gz
+ ├── explainthisrepo-v0.29.0-cli-installer-linux-arm64.tar.gz.sha256
+ ├── explainthisrepo-v0.29.0-cli-installer-darwin-arm64.tar.gz
+ ├── explainthisrepo-v0.29.0-cli-installer-darwin-arm64.tar.gz.sha256
+ ├── explainthisrepo-v0.29.0-cli-installer-darwin-x64.tar.gz
+ ├── explainthisrepo-v0.29.0-cli-installer-darwin-x64.tar.gz.sha256
+ ├── explainthisrepo-v0.29.0-cli-installer-win-x64.zip
+ ├── explainthisrepo-v0.29.0-cli-installer-win-x64.zip.sha256
+ ├── explainthisrepo-v0.29.0-cli-installer-win-arm64.zip
+ └── explainthisrepo-v0.29.0-cli-installer-win-arm64.zip.sha256
+```
+
+## 7. GitHub Actions pipeline
+
+A release starts with a semantic version tag:
+
+```
+git tag v0.29.0
+git push origin v0.29.0
+```
+
+The workflow is triggered by the tag.
+
+```
+v0.29.0
+ ↓
+GitHub Actions
+```
+
+### Build stage
+
+The six matrix jobs run independently:
+
+```
+darwin-arm64
+darwin-x64
+linux-arm64
+linux-x64
+win-arm64
+win-x64
+```
+
+Each job:
+
+1. Checks out the repository.
+2. Installs Python.
+3. Validates the tag against the canonical project version.
+4. Installs build dependencies.
+5. Runs `scripts/build_pyinstaller.py`.
+6. Generates a binary SHA256 checksum.
+7. Verifies the checksum.
+8. Uploads the native binary and checksum as a workflow artifact.
+
+If one target fails, the other targets can finish, but the release job does not run until the required build matrix succeeds.
+
+## 8. Release stage
+
+The release job downloads all six build artifacts.
+
+It then reconstructs:
+
+```
+node_version/dist/native/
+dotnet_version/native/
+```
+
+This is important because the native binaries are also consumed by the npm and .NET distributions.
+
+The release job then creates the human-readable release assets.
+
+After that it creates the CLI installer archives.
+
+The installer archives are generated from the already-validated native binaries.
+
+The pipeline therefore does not build a second copy of the executable.
+
+```
+validated binary
+ │
+ ├── npm package
+ │
+ ├── NuGet package
+ │
+ ├── human-readable release asset
+ │
+ └── CLI installer archive
+```
+
+## 9. Archive packaging
+
+Unix targets use:
+
+`.tar.gz`
+
+Windows targets use:
+
+`.zip`
+
+Examples:
+
+```
+explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz
+explainthisrepo-v0.29.0-cli-installer-linux-arm64.tar.gz
+explainthisrepo-v0.29.0-cli-installer-darwin-x64.tar.gz
+explainthisrepo-v0.29.0-cli-installer-darwin-arm64.tar.gz
+explainthisrepo-v0.29.0-cli-installer-win-x64.zip
+explainthisrepo-v0.29.0-cli-installer-win-arm64.zip
+```
+
+The archive is created from a temporary staging directory so unrelated files cannot accidentally enter the archive.
+
+The pipeline then inspects the archive contents.
+
+An archive containing unexpected files causes the release job to fail.
+
+## 10. Installer
+
+The stable installation entry point is:
+
+```bash
+curl -fsSL https://cli.explainthisrepo.com/install.sh | sh
+```
+
+The installer owns platform selection.
+
+The release pipeline's responsibility is to publish correctly named artifacts.
+
+The installer performs:
+
+```
+uname
+ ↓
+operating system
+ ↓
+CPU architecture
+ ↓
+target
+ ↓
+latest stable version
+ ↓
+archive filename
+ ↓
+download
+ ↓
+SHA256 verification
+ ↓
+extraction
+ ↓
+installation
+```
+
+For example:
+
+```
+macOS Apple Silicon
+ ↓
+darwin-arm64
+ ↓
+v0.29.0
+ ↓
+explainthisrepo-v0.29.0-cli-installer-darwin-arm64.tar.gz
+```
+
+## 11. Why the installer owns platform selection
+
+The installer runs on the user's machine.
+
+It knows:
+
+```
+uname -s
+uname -m
+```
+
+Therefore it is the correct place to determine:
+
+```
+OS
+architecture
+target
+```
+
+The GitHub Release does not need to understand the user's machine.
+
+It only needs to expose the deterministic artifacts.
+
+This keeps the system boundary clean:
+
+```
+Release system
+ │
+ │ produces artifacts
+ ▼
+GitHub Release
+ │
+ │ serves artifacts
+ ▼
+Installer
+ │
+ │ selects artifact
+ ▼
+User machine
+```
+
+## 12. Stable installer URL
+
+The public installer URL is:
+
+```
+https://cli.explainthisrepo.com/install.sh
+```
+
+The URL should be treated as a stable API.
+
+Users should not need to know:
+
+- the GitHub repository name
+- the latest version
+- the release tag
+- the archive filename
+- the architecture mapping
+- the download URL
+
+The installer abstracts those details.
+
+## 13. Version resolution
+
+The installer queries GitHub's latest release endpoint.
+
+The endpoint returns the latest published full release rather than a draft or prerelease.
+
+The installer extracts the release tag, for example:
+
+`v0.29.0`
+
+It then constructs the expected artifact name:
+
+`explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz`
+
+The version is therefore part of the artifact URL but does not need to be hardcoded into "install.sh".
+
+## 14. Failure behavior
+
+The distribution system is designed to fail closed.
+
+Examples:
+
+```
+unsupported OS
+ → stop
+
+unsupported architecture
+ → stop
+
+GitHub unavailable
+ → stop
+
+latest release cannot be determined
+ → stop
+
+archive unavailable
+ → stop
+
+checksum unavailable
+ → stop
+
+checksum mismatch
+ → stop
+
+invalid archive contents
+ → stop
+
+installation permission failure
+ → stop
+```
+
+The installer must never silently install a different platform's binary.
+
+## 15. Important system invariants
+
+These properties must remain true.
+
+### Invariant 1: Target naming
+
+The supported internal target names are:
+
+```
+darwin-arm64
+darwin-x64
+linux-arm64
+linux-x64
+win-arm64
+win-x64
+```
+
+### Invariant 2: Archive naming
+
+The naming contract is:
+
+`explainthisrepo-v-cli-installer-.`
+
+where:
+
+```
+Linux/macOS → tar.gz
+Windows → zip
+```
+
+### Invariant 3: Archive contents
+
+Unix:
+
+`explainthisrepo`
+
+Windows:
+
+`explainthisrepo.exe`
+
+Nothing else.
+
+### Invariant 4: Checksums
+
+Every installer archive has a separate SHA256 checksum.
+
+### Invariant 5: Human-readable release assets
+
+The existing human-readable binary names remain unchanged.
+
+### Invariant 6: Native build source
+
+`scripts/build_pyinstaller.py` remains the source of the native executable build.
+
+### Invariant 7: Installer independence
+
+The standalone installer must not require:
+
+```
+Python
+pip
+Node.js
+npm
+```
+
+## 16. Adding another platform
+
+Adding a new platform requires changes to the distribution contract.
+
+At minimum:
+
+1. Add the target to the GitHub Actions matrix.
+2. Ensure `scripts/build_pyinstaller.py` supports it.
+3. Define its human-readable release asset name.
+4. Define its installer archive format.
+5. Add archive packaging.
+6. Add checksum generation.
+7. Add installer OS/architecture mapping.
+8. Add release-layout validation.
+9. Add the platform to the supported-platform documentation.
+10. Test installation on a clean machine.
+
+The target should not be considered supported until the complete path works:
+
+```
+build
+ ↓
+checksum
+ ↓
+archive
+ ↓
+release
+ ↓
+installer selection
+ ↓
+download
+ ↓
+verification
+ ↓
+installation
+ ↓
+execution
+```
+
+## 17. Release testing
+
+Before treating a new distribution change as complete, test the actual user path.
+
+Linux:
+
+```bash
+curl -fsSL https://cli.explainthisrepo.com/install.sh | sh
+```
+
+Widows (with Git Bash/WSL)
+
+```bash
+curl -fsSL https://cli.explainthisrepo.com/install.sh | sh
+```
+
+macOS:
+
+```bash
+curl -fsSL https://cli.explainthisrepo.com/install.sh | sh
+```
+
+Then:
+
+```bash
+explainthisrepo
+```
+
+The test environment should be as close as possible to a machine that has never installed ExplainThisRepo before.
+
+The important test is not:
+
+`Did PyInstaller succeed?`
+
+The important test is:
+
+`Can a new machine install and execute the correct binary?`
+
+## 18. Distribution system
+
+The complete system is:
+
+```
+ git tag v0.29.0
+ │
+ ▼
+ GitHub Actions
+ │
+ ┌────────────┴────────────┐
+ │ │
+ ▼ ▼
+ Build six targets Validate checksums
+ │ │
+ └────────────┬────────────┘
+ ▼
+ Native binaries
+ │
+ ┌────────────┼────────────┐
+ │ │ │
+ ▼ ▼ ▼
+ npm NuGet Release assets
+ │
+ ┌───────┴────────┐
+ │ │
+ ▼ ▼
+ Human binary CLI installer
+ archive
+ │
+ ▼
+ GitHub Release
+ │
+ ▼
+ cli.explainthisrepo.com
+ │
+ ▼
+ install.sh
+ │
+ ┌──────────────┴──────────────┐
+ │ │
+ ▼ ▼
+ OS + arch latest version
+ │ │
+ └──────────────┬──────────────┘
+ ▼
+ archive URL
+ │
+ ▼
+ download
+ │
+ ▼
+ SHA256 verify
+ │
+ ▼
+ extract
+ │
+ ▼
+ /usr/local/bin/explainthisrepo
+ │
+ ▼
+ explainthisrepo
+```
+
+The CLI is the payload.
+
+The release pipeline is the distribution mechanism.
+
+The installer is the machine-facing interface to that distribution mechanism.
+
+> The release pipeline described here is implemented by [RELEASE-ARCHITECTURE.md](RELEASE-ARCHITECTURE.md)
\ No newline at end of file
diff --git a/docs/release-architecture.md b/docs/release-architecture.md
index e30b9d5..d37966d 100644
--- a/docs/release-architecture.md
+++ b/docs/release-architecture.md
@@ -2,7 +2,18 @@
This document describes the full release pipeline.
-It is a deterministic, multi-language build, release and publishing system with build-time materialization, artifact fan-out, multi-registry publishing, integrity verification and more.
+It is a deterministic, multi-language build, release and publishing system with:
+
+- one canonical version source
+- build-time version materialization
+- six-target native builds
+- artifact fan-out
+- artifact rehydration
+- multi-registry publishing
+- native binary integrity verification
+- installer archive generation
+- installer archive integrity verification
+- GitHub Release publication
## Core Principle
@@ -14,7 +25,15 @@ The system enforces:
- One canonical version extracted per release
- One materialization phase before any build tool runs
- One consistent version propagated across all ecosystems
-- pyproject.toml is the ONLY human-edited version source; So humans (contributors) should only touch pyproject.toml
+- One native binary build per target
+- One validated native binary reused across distribution formats
+- One release job responsible for aggregation and publication
+
+`pyproject.toml` is the ONLY human-edited version source
+
+Contributors should only change the version in `pyproject.toml`.
+
+CI materializes that version into the files required by the npm and .NET packaging systems.
## System Overview
@@ -24,87 +43,244 @@ The pipeline is split into three phases:
2. Materialization phase (version + manifest normalization)
3. Release phase (publishing + verification)
+
+The build stage runs six native builds in parallel.
+
+The materialization stage runs once after all six builds succeed, normalizing versions and manifests before assembling the release artifacts.
+
+The release stage runs once after all six builds succeed.
+
+The overall system is:
+
+```
+pyproject.toml
+ ↓
+Git tag
+ ↓
+GitHub Actions
+ ↓
+┌──────────────────────────────────────────────┐
+│ Build matrix │
+│ │
+│ darwin-arm64 darwin-x64 │
+│ linux-arm64 linux-x64 │
+│ win-arm64 win-x64 │
+│ │
+└──────────────────────────────────────────────┘
+ ↓
+validated native binaries
+ ↓
+artifact rehydration
+ ↓
+┌──────────────┬──────────────┬────────────────┐
+│ npm package │ NuGet tool │ GitHub Release │
+└──────────────┴──────────────┴────────────────┘
+ ↓
+ release distribution
+ ↓
+ human-readable binaries
+ +
+ CLI installer archives
+```
+
## 1. Build Phase (Matrix Execution)
-Runs per target:
+The build phase runs once for each supported target.
+
+The current build matrix contains six targets:
- darwin-arm64
-- linux-x64
+- darwin-x64
- linux-arm64
+- linux-x64
+- win-arm64
- win-x64
-Steps:
+Each target has its own GitHub Actions runner.
+
+Current runner mapping:
-- Checkout repository
-- Setup Python environment
-- Install dependencies
-- Build PyInstaller binary
-- Generate native checksums
-- Upload artifacts per platform
+```
+darwin-arm64 → macos-latest
+darwin-x64 → macos-15-intel
-Output:
+linux-x64 → ubuntu-latest
+linux-arm64 → ubuntu-24.04-arm
-- Platform-specific native binaries
-- SHA256 checksum files
+win-arm64 → windows-11-arm
+win-x64 → windows-latest
+```
-Important constraint:
+The matrix uses:
+
+```
+fail-fast: false
+```
-No version rewriting happens here.
-This phase is intentionally isolated from release logic.
+This allows the individual matrix jobs to finish independently when another target fails.
-## 2. Release Phase (Canonical Pipeline)
-Runs once after all build jobs complete.
+However, the `release` job depends on the complete `build` job, so a failed required target prevents the release stage from running.
-### Step 1: Canonical Version Extraction
+### Build steps
-```text
-pyproject.toml → CI extracts version → .ci/version.txt
+Each build job:
+
+1. Checks out the repository.
+2. Sets up Python 3.12.
+3. Validates the Git tag against the canonical project version.
+4. Installs Python dependencies.
+5. Runs `scripts/build_pyinstaller.py`.
+6. Generates a SHA256 checksum for the native binary.
+7. Uploads the native binary and checksum as a workflow artifact.
+
+The output for each target is:
+
+- native binary
+- native binary SHA256 checksum
+
+The native build output is placed under:
+
+```
+node_version/dist/native//
```
-This value becomes immutable for the rest of the pipeline.
+Examples:
-No other file is a source of truth.
+```
+node_version/dist/native/linux-x64/
+└── explainthisrepo
+
+node_version/dist/native/linux-arm64/
+└── explainthisrepo
+
+node_version/dist/native/darwin-x64/
+└── explainthisrepo
+
+node_version/dist/native/darwin-arm64/
+└── explainthisrepo
+
+node_version/dist/native/win-x64/
+└── explainthisrepo.exe
+
+node_version/dist/native/win-arm64/
+└── explainthisrepo.exe
+```
+
+### Build isolation
+
+The build phase does not rewrite the repository's version-dependent manifests.
+
+Its responsibility is native artifact generation.
+
+Version materialization for npm, .NET, and runtime version files happens in the release job.
+
+
+## 2. Canonical Version Extraction
+The release job extracts the canonical version from:
-### Step 2: Tag Validation Gate
+```
+pyproject.toml
+ ↓
+scripts/get_version.py
+ ↓
+.ci/version.txt
+```
-Ensures release is intentional and consistent:
+The resulting value becomes the canonical release version for the remainder of the release job.
-- Git tag must exist
+For example:
-- Tag version must match canonical version
+```
+pyproject.toml
+ ↓
+0.29.0
+ ↓
+.ci/version.txt
+```
+No other project file becomes a version source of truth.
-If mismatch occurs:
-- pipeline fails immediately
+## 3. Tag Validation Gate
-### Step 3: Manifest Materialization (Critical Step)
+The workflow requires a Git tag.
-CI rewrites all version-dependent artifacts BEFORE any tool executes.
+The tag is expected to use the form:
-Rewritten files:
+```
+v
+```
-- node_version/package.json
+For example:
-- node_version/package-lock.json
+```
+v0.29.0
+```
-- dotnet_version/ExplainThisRepo.csproj
+The workflow removes the leading `v` and compares the result with the canonical version extracted from `pyproject.toml`.
-- runtime version files (_version.py variants)
+```
+Git tag: v0.29.0
+ ↓
+ 0.29.0
+pyproject.toml: 0.29.0
+
+ ↓
+ MATCH
+```
+
+If they do not match, the pipeline stops.
+
+This prevents a release tag from publishing packages whose versions disagree with the canonical project version.
+
+
+## 4. Manifest Materialization
+
+After canonical version extraction and tag validation, CI rewrites version-dependent files.
+
+The current materialized files are:
+
+- `node_version/package.json`
+- `node_version/package-lock.json`
+- `dotnet_version/ExplainThisRepo.csproj`
+
+- runtime version files:
+ - `_version.py
+ - `explain_this_repo/_version.py`
+ - `node_version/_version.py`
+
+The materialization flow is:
+
+```
+.ci/version.txt
+ ↓
+┌──────────────────────────────────────┐
+│ version-dependent project artifacts │
+└──────────────────────────────────────┘
+ ↓
+npm metadata
+.NET metadata
+runtime version files
+```
+
+The purpose is to ensure that every publishing ecosystem receives the same release version.
Rules:
+- `pyproject.toml` remains the source of truth.
- No tool sees unmaterialized state
-
- No dependency install occurs before rewrite
-
- All ecosystems receive identical version
+- CI performs the version propagation.
+- npm does not determine the release version.
+- NuGet does not determine the release version.
+- Runtime version files are generated from the canonical version.
-### Step 4: Dependency Restoration
+## 5. Dependency Restoration
After materialization:
@@ -119,144 +295,594 @@ At this point:
All tools operate on a fully normalized filesystem state.
-### Step 5: Artifact Rehydration
-Downloaded build outputs are merged:
+## 6. Native Artifact Rehydration
+
+The release job downloads the six build artifacts.
+
+They are then rehydrated into the repository's distribution trees:
+
+- `node_version/dist/native//`
+- `dotnet_version/native//`
+
+A third staging location is created:
+
+- `release/`
+
+The native binaries are therefore reused by multiple distribution systems.
+
+The release job does not rebuild them.
+
+This creates a unified artifact tree.:
+
+```
+GitHub Actions build artifacts
+ ↓
+ artifact download
+ ↓
+ artifact rehydration
+ ↓
+┌────────────┼─────────────┐
+│ │ │
+▼ ▼ ▼
+npm .NET GitHub Release
+```
+
+
+## 7. Native Binary Integrity Verification
+
+Before publishing, the release job verifies the SHA256 checksums generated during the build phase.
+
+The verification operates against the rehydrated native binaries.
+
+The flow is:
+
+```
+native binary
+ +
+native binary.sha256
+ ↓
+SHA256 verification
+ ↓
+continue only if valid
+```
+
+If verification fails, the release job stops.
+
+This creates an integrity gate between native artifact generation and publication.
+
+
+## 8. Human-Readable GitHub Release Assets
+
+Each validated native binary is also copied into the `release/` staging directory using a human-readable filename.
+
+The current names are:
+
+```
+ExplainThisRepo-Linux-x64
+ExplainThisRepo-Linux-x64.sha256
+
+ExplainThisRepo-Linux-ARM64
+ExplainThisRepo-Linux-ARM64.sha256
+
+ExplainThisRepo-macOS-Apple-Silicon-arm64
+ExplainThisRepo-macOS-Apple-Silicon-arm64.sha256
+
+ExplainThisRepo-macOS-Intel-x64
+ExplainThisRepo-macOS-Intel-x64.sha256
+
+ExplainThisRepo-Windows-ARM64.exe
+ExplainThisRepo-Windows-ARM64.exe.sha256
+
+ExplainThisRepo-Windows-x64.exe
+ExplainThisRepo-Windows-x64.exe.sha256
+```
+
+These assets are intended for people browsing a GitHub Release and wanting the native executable directly.
+
+Their names are deliberately human-readable.
-- node_version/dist/native/
+They are separate from the machine-oriented target identifiers used internally by CI.
-- dotnet_version/native/
-- release/ staging folder
+## 9. CLI Installer Archives
-This creates a unified artifact tree.
+The release pipeline also packages the validated native binaries into deterministic CLI installer archives.
-### Step 6: Integrity Verification
+The archive naming convention is:
-Hard validation gates:
+```
+explainthisrepo-v-cli-installer-.
+```
+
+Current archives:
+
+```
+explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz
+explainthisrepo-v0.29.0-cli-installer-linux-arm64.tar.gz
+
+explainthisrepo-v0.29.0-cli-installer-darwin-arm64.tar.gz
+explainthisrepo-v0.29.0-cli-installer-darwin-x64.tar.gz
+
+explainthisrepo-v0.29.0-cli-installer-win-x64.zip
+explainthisrepo-v0.29.0-cli-installer-win-arm64.zip
+```
+
+Unix targets use:
+
+`.tar.gz`
+
+Windows targets use:
-- SHA256 checksum verification
+`.zip`
-- File presence validation
+The installer archive is created from the already-built native binary.
-- Native binary existence checks
+There is no second PyInstaller build.
+The same validated native executable fans out into multiple distribution formats:
+
+```
+ validated native binary
+ │
+ ┌──────────────┼───────────────┐
+ │ │ │
+ ▼ ▼ ▼
+ npm package NuGet GitHub Release
+ │
+ ┌───────┴────────┐
+ │ │
+ ▼ ▼
+ raw executable installer archive
+```
-If any check fails: release is aborted
-### Step 7: Node.js Packaging Pipeline
+## 10. Installer Archive Contract
-Steps:
+Each installer archive has a strict content contract.
-- version already injected from CI materialization
+Linux and macOS archives contain exactly:
-- npm ci installs dependencies
+`explainthisrepo`
-- metadata sync step runs
+Windows archives contain exactly:
-- npm pack generates tarball
+`explainthisrepo.exe`
-- tarball validated for native binaries
+The archive contains no:
-- npm publish executes
+- nested directories
+- README files
+- version files
+- metadata files
+- checksum files
+The checksum is published as a separate GitHub Release asset.
+
+This makes extraction deterministic.
+
+The installer knows the executable name in advance and does not need to search the extracted directory.
+
+
+# 11. Installer Archive Integrity
+
+Each installer archive receives its own SHA256 checksum.
+
+For example:
+
+```
+explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz
+explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz.sha256
+```
+
+The archive checksum is separate from the checksum of the raw native binary.
+
+The installation integrity flow is:
+
+```
+download archive
+ ↓
+download archive checksum
+ ↓
+verify archive SHA256
+ ↓
+extract
+ ↓
+install executable
+```
+
+The archive itself is therefore the integrity boundary used by the standalone installer.
+
+
+## 12. npm Distribution
+
+The rehydrated native binaries are included in the npm package.
+
+The npm release pipeline:
+
+1. Uses the CI-materialized version.
+2. Checks whether the version already exists.
+3. Runs `npm ci`.
+4. Runs `npm run sync-meta`.
+5. Runs `npm pack`.
+6. Verifies that the package contains native binaries.
+7. Runs the package check.
+8. Publishes to npm.
+
+The npm package does not build the native executables.
+
+It consumes the binaries produced by the native build matrix.
Invariant:
-npm never determines version itself. It only consumes CI-materialized state.
+```
+PyInstaller build
+ ↓
+validated native binaries
+ ↓
+npm package
+```
-### Step 8: .NET Packaging Pipeline
-Steps:
+## 13. .NET Distribution
-- csproj already rewritten by CI
+The rehydrated native binaries are also included in the .NET Global Tool package.
-- dotnet pack generates NuGet package
+The .NET release pipeline:
-- version existence check against NuGet registry
+1. Uses the CI-materialized `.csproj` version.
+2. Checks whether the version already exists on NuGet.
+3. Runs `dotnet pack`.
+4. Publishes the generated package to NuGet.
-- dotnet nuget push publishes package
+The .NET package does not build a separate copy of the native executable.
+It consumes the native binaries produced by the build matrix.
Invariant:
-NuGet package version is CI-derived, not project-derived at runtime.
+```
+PyInstaller build
+ ↓
+validated native binaries
+ ↓
+NuGet package
+```
+
-### Step 9: GitHub Release Publication
+## 14. GitHub Release Publication
-Final step:
+The final release stage publishes everything staged under:
-- Upload all artifacts in `release/`
+`release/`
-- Generate release notes
+The GitHub Release contains:
-- Publish GitHub release
+```
+Human-readable native binaries
++
+native binary checksums
++
+CLI installer archives
++
+installer archive checksums
+```
+GitHub Releases therefore act as the public artifact distribution layer.
-This is the user-facing artifact layer.
+A release such as `v0.29.0` contains both artifact classes:
+
+```
+GitHub Release v0.29.0
+│
+├── Human-readable native binaries
+│ ├── ExplainThisRepo-Linux-x64
+│ ├── ExplainThisRepo-Linux-x64.sha256
+│ ├── ExplainThisRepo-Linux-ARM64
+│ ├── ExplainThisRepo-Linux-ARM64.sha256
+│ ├── ExplainThisRepo-macOS-Apple-Silicon-arm64
+│ ├── ExplainThisRepo-macOS-Apple-Silicon-arm64.sha256
+│ ├── ExplainThisRepo-macOS-Intel-x64
+│ ├── ExplainThisRepo-macOS-Intel-x64.sha256
+│ ├── ExplainThisRepo-Windows-ARM64.exe
+│ ├── ExplainThisRepo-Windows-ARM64.exe.sha256
+│ ├── ExplainThisRepo-Windows-x64.exe
+│ └── ExplainThisRepo-Windows-x64.exe.sha256
+│
+└── CLI installer archives
+ ├── explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz
+ ├── explainthisrepo-v0.29.0-cli-installer-linux-x64.tar.gz.sha256
+ ├── explainthisrepo-v0.29.0-cli-installer-linux-arm64.tar.gz
+ ├── explainthisrepo-v0.29.0-cli-installer-linux-arm64.tar.gz.sha256
+ ├── explainthisrepo-v0.29.0-cli-installer-darwin-arm64.tar.gz
+ ├── explainthisrepo-v0.29.0-cli-installer-darwin-arm64.tar.gz.sha256
+ ├── explainthisrepo-v0.29.0-cli-installer-darwin-x64.tar.gz
+ ├── explainthisrepo-v0.29.0-cli-installer-darwin-x64.tar.gz.sha256
+ ├── explainthisrepo-v0.29.0-cli-installer-win-x64.zip
+ ├── explainthisrepo-v0.29.0-cli-installer-win-x64.zip.sha256
+ ├── explainthisrepo-v0.29.0-cli-installer-win-arm64.zip
+ └── explainthisrepo-v0.29.0-cli-installer-win-arm64.zip.sha256
+```
-## System Model
+## 15. System Model
-This system behaves like a compiler pipeline:
+The release system behaves like a compiler pipeline.
```
pyproject.toml
- ↓
-version extraction (compiler frontend)
- ↓
-manifest materialization (IR generation)
- ↓
-build tools (execution stage)
- ↓
+ ↓
+canonical version extraction
+ ↓
+Git tag validation
+ ↓
+version materialization
+ ↓
+native build matrix
+ ↓
artifact aggregation
- ↓
-publishing targets (npm, NuGet, GitHub)
+ ↓
+integrity verification
+ ↓
+distribution fan-out
+ ↓
+┌──────────────┬──────────────┬──────────────────┐
+│ npm │ NuGet │ GitHub Release │
+│ │ │ │
+│ native │ native │ raw binaries │
+│ binaries │ binaries │ installer │
+│ │ │ archives │
+└──────────────┴──────────────┴──────────────────┘
```
-## Critical Invariant
+The important distinction is that native compilation happens once per target.
-At no point may a build tool execute against raw repository state.
+Distribution packaging happens afterward.
-Materialization MUST always occur before:
-- npm ci
+## 16. Critical Invariants
-- dotnet restore / pack
+#### Invariant 1: Canonical version
-- npm pack
+`pyproject.toml` is the only human-edited version source.
-- nuget publish
+#### Invariant 2: Tag consistency
-- PyInstaller
+The Git tag must match the canonical project version.
-- any version-sensitive operation
+#### Invariant 3: Six supported native targets
-## Failure Modes Prevented
+The current target contract is:
-This architecture eliminates:
+```
+darwin-arm64
+darwin-x64
+linux-arm64
+linux-x64
+win-arm64
+win-x64
+```
+
+#### Invariant 4: One native build per target
+
+The native executable is built once by:
+
+`scripts/build_pyinstaller.py`
+
+All later distribution formats consume that built executable.
+
+#### Invariant 5: Native artifact integrity
+
+Native binary checksums must verify before publication continues.
+
+#### Invariant 6: Installer archive naming
+
+The archive naming contract is:
+
+`explainthisrepo-v-cli-installer-.`
+
+#### Invariant 7: Installer archive contents
+
+Unix archives contain:
+
+```
+explainthisrepo
+```
+
+Windows archives contain:
+
+```
+explainthisrepo.exe
+```
+
+Nothing else.
+
+#### Invariant 8: Installer archive integrity
+
+Every installer archive has a separate SHA256 checksum.
+
+#### Invariant 9: Human-readable release assets
+
+The existing human-readable native binary names remain stable.
+
+#### Invariant 10: Distribution independence
+
+The standalone installer does not require:
-- cross-language version drift
+Python
+pip
+Node.js
+npm
-- tag/package mismatch errors
-- manual sync errors between ecosystems
+## 17. Failure Modes Prevented
-- inconsistent build outputs
+The architecture prevents or detects:
-- “works locally but fails in CI” version bugs
+- canonical version drift
+- Git tag/package version mismatch
+- cross-language version mismatch
+- missing native binaries
+- corrupted native artifacts
+- missing release targets
+- incorrectly named installer archives
+- invalid installer archive contents
+- missing installer checksums
+- checksum mismatches
+- accidental publication of unvalidated native artifacts
+- rebuilding different native binaries for different distribution formats
-## Design Summary
+The release job stops when a required validation fails.
-This is not a CI script.
-It is a deterministic release compiler that:
+## 18. Adding Another Platform
-- extracts a canonical version
+Adding another platform requires changes across the complete distribution path.
+
+At minimum:
+
+1. Add the target to the GitHub Actions matrix.
+2. Ensure `scripts/build_pyinstaller.py` supports the target.
+3. Define the runner.
+4. Define the human-readable release asset name.
+5. Define the executable filename.
+6. Define the installer archive format.
+7. Add release packaging for the target.
+8. Add checksum generation and verification.
+9. Add installer target mapping.
+10. Add release-layout validation.
+11. Update supported-platform documentation.
+12. Test installation on a clean machine.
+
+A platform is not complete when its binary builds.
+
+The complete path must work:
+
+```
+build
+ ↓
+checksum
+ ↓
+artifact upload
+ ↓
+artifact rehydration
+ ↓
+archive packaging
+ ↓
+release publication
+ ↓
+installer selection
+ ↓
+download
+ ↓
+verification
+ ↓
+installation
+ ↓
+execution
+```
+
+
+## 19. Release Testing
+
+The important test is the complete user path, not only whether PyInstaller succeeds.
+
+For Unix-like environments, test:
+
+
+```bash
+curl -fsSL https://cli.explainthisrepo.com/install.sh | sh
+```
+
+Then:
+
+```bash
+explainthisrepo
+```
+
+Testing should cover the supported native targets and should be performed against environments that are as close as possible to fresh machines.
+
+The key question is:
+
+`Can a new machine receive the correct native executable from the published release and execute it successfully?`
+
+
+## 20. Complete Distribution System
+
+The complete release system is:
+
+```
+ pyproject.toml
+ │
+ ▼
+ Git tag v0.29.0
+ │
+ ▼
+ GitHub Actions
+ │
+ ┌──────────┴──────────┐
+ │ │
+ ▼ ▼
+ Build six targets Tag validation
+ │
+ ▼
+ Native binaries
+ │
+ ▼
+ Native checksums
+ │
+ ▼
+ Artifact upload
+ │
+ ▼
+ Release job
+ │
+ ┌─────────┴─────────┐
+ │ │
+ ▼ ▼
+ Version materialization Artifact rehydration
+ │ │
+ └─────────┬─────────┘
+ ▼
+ Integrity checks
+ │
+ ▼
+ Distribution fan-out
+ │
+ ┌────────────┼───────────────────┐
+ │ │ │
+ ▼ ▼ ▼
+ npm NuGet GitHub Release
+ │
+ ┌─────────────┴─────────────┐
+ │ │
+ ▼ ▼
+ Human-readable CLI installer
+ binaries archives
+ │ │
+ │ SHA256 checksums
+ │ │
+ └─────────────┬─────────────┘
+ ▼
+ GitHub Release
+ │
+ ▼
+ cli.explainthisrepo.com
+ │
+ ▼
+ install.sh
+ │
+ ┌─────────┴─────────┐
+ │ │
+ ▼ ▼
+ OS + arch latest release
+ │ │
+ └─────────┬─────────┘
+ ▼
+```
-- materializes a consistent build state
-- executes all tooling on normalized inputs
+## Release Distribution
-- produces reproducible multi-ecosystem releases
\ No newline at end of file
+For the artifact packaging, distribution model, installer archives, and installation flow, see [RELEASE-DISTRIBUTION.md](RELEASE-DISTRIBUTION.md).
\ No newline at end of file
diff --git a/install.sh b/install.sh
new file mode 100644
index 0000000..32dda8d
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,320 @@
+#!/usr/bin/env sh
+
+set -eu
+
+REPO="calchiwo/ExplainThisRepo"
+BINARY_NAME="explainthisrepo"
+API_URL="https://api.github.com/repos/${REPO}/releases/latest"
+RELEASE_BASE_URL="https://github.com/${REPO}/releases/download"
+
+INSTALL_DIR="${EXPLAINTHISREPO_INSTALL_DIR:-/usr/local/bin}"
+
+TMP_DIR=""
+
+cleanup() {
+ if [ -n "${TMP_DIR}" ] && [ -d "${TMP_DIR}" ]; then
+ rm -rf "${TMP_DIR}"
+ fi
+}
+
+trap cleanup EXIT INT TERM
+
+fail() {
+ echo "Error: $*" >&2
+ exit 1
+}
+
+info() {
+ echo "==> $*"
+}
+
+require_command() {
+ command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
+}
+
+# Prerequisites
+
+require_command uname
+require_command curl
+require_command tar
+require_command mktemp
+require_command chmod
+require_command mv
+require_command mkdir
+require_command rm
+
+# sha256sum exists on Linux.
+# shasum is normally available on macOS.
+if command -v sha256sum >/dev/null 2>&1; then
+ SHA256_TOOL="sha256sum"
+elif command -v shasum >/dev/null 2>&1; then
+ SHA256_TOOL="shasum"
+else
+ fail "Neither sha256sum nor shasum is available"
+fi
+
+# Detect operating system
+
+OS="$(uname -s)"
+
+case "${OS}" in
+ Linux)
+ OS_TARGET="linux"
+ ;;
+
+ Darwin)
+ OS_TARGET="darwin"
+ ;;
+
+ *)
+ fail "Unsupported operating system: ${OS}. Supported systems are Linux and macOS."
+ ;;
+esac
+
+# Detect architecture
+
+MACHINE="$(uname -m)"
+
+case "${MACHINE}" in
+ x86_64|amd64)
+ ARCH="x64"
+ ;;
+
+ arm64|aarch64)
+ ARCH="arm64"
+ ;;
+
+ *)
+ fail "Unsupported architecture: ${MACHINE}. Supported architectures are x64 and arm64."
+ ;;
+esac
+
+TARGET="${OS_TARGET}-${ARCH}"
+
+# Resolve latest release
+
+info "Detecting latest ExplainThisRepo release..."
+
+RELEASE_JSON="$(
+ curl \
+ --fail \
+ --silent \
+ --show-error \
+ --location \
+ --retry 3 \
+ --retry-delay 1 \
+ --header "Accept: application/vnd.github+json" \
+ --header "X-GitHub-Api-Version: 2026-03-10" \
+ "${API_URL}"
+)" || fail "Could not contact GitHub to determine the latest release."
+
+VERSION="$(
+ printf '%s\n' "${RELEASE_JSON}" |
+ sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' |
+ head -n 1
+)"
+
+[ -n "${VERSION}" ] ||
+ fail "Could not determine the latest ExplainThisRepo release."
+
+case "${VERSION}" in
+ v[0-9]*)
+ ;;
+ *)
+ fail "GitHub returned an invalid release tag: ${VERSION}"
+ ;;
+esac
+
+# Construct artifact names
+
+ARCHIVE_NAME="${BINARY_NAME}-${VERSION}-cli-installer-${TARGET}.tar.gz"
+CHECKSUM_NAME="${ARCHIVE_NAME}.sha256"
+
+ARCHIVE_URL="${RELEASE_BASE_URL}/${VERSION}/${ARCHIVE_NAME}"
+CHECKSUM_URL="${RELEASE_BASE_URL}/${VERSION}/${CHECKSUM_NAME}"
+
+info "Release: ${VERSION}"
+info "Platform: ${TARGET}"
+info "Archive: ${ARCHIVE_NAME}"
+
+# Temporary workspace
+
+TMP_DIR="$(mktemp -d 2>/dev/null || mktemp -d -t explainthisrepo)"
+
+ARCHIVE_PATH="${TMP_DIR}/${ARCHIVE_NAME}"
+CHECKSUM_PATH="${TMP_DIR}/${CHECKSUM_NAME}"
+EXTRACT_DIR="${TMP_DIR}/extracted"
+
+mkdir -p "${EXTRACT_DIR}"
+
+# Download archive
+
+info "Downloading installer archive..."
+
+curl \
+ --fail \
+ --silent \
+ --show-error \
+ --location \
+ --retry 3 \
+ --retry-delay 1 \
+ --output "${ARCHIVE_PATH}" \
+ "${ARCHIVE_URL}" ||
+ fail "Could not download ${ARCHIVE_NAME}"
+
+# Download checksum
+
+info "Downloading SHA256 checksum..."
+
+curl \
+ --fail \
+ --silent \
+ --show-error \
+ --location \
+ --retry 3 \
+ --retry-delay 1 \
+ --output "${CHECKSUM_PATH}" \
+ "${CHECKSUM_URL}" ||
+ fail "Could not download ${CHECKSUM_NAME}"
+
+# Verify archive integrity
+
+info "Verifying archive integrity..."
+
+case "${SHA256_TOOL}" in
+ sha256sum)
+ (
+ cd "${TMP_DIR}"
+ sha256sum -c "${CHECKSUM_NAME}"
+ ) || fail "SHA256 verification failed."
+ ;;
+
+ shasum)
+ EXPECTED_HASH="$(
+ sed -n 's/^\([0-9a-fA-F]\{64\}\).*/\1/p' "${CHECKSUM_PATH}" |
+ head -n 1
+ )"
+
+ [ -n "${EXPECTED_HASH}" ] ||
+ fail "Invalid SHA256 checksum file."
+
+ ACTUAL_HASH="$(
+ shasum -a 256 "${ARCHIVE_PATH}" |
+ awk '{print $1}'
+ )"
+
+ if [ "${EXPECTED_HASH}" != "${ACTUAL_HASH}" ]; then
+ fail "SHA256 verification failed."
+ fi
+ ;;
+esac
+
+info "SHA256 verification passed."
+
+# Extract archive
+
+info "Extracting archive..."
+
+tar \
+ --extract \
+ --gzip \
+ --file "${ARCHIVE_PATH}" \
+ --directory "${EXTRACT_DIR}" ||
+ fail "Could not extract ${ARCHIVE_NAME}"
+
+# Validate archive contents
+
+EXPECTED_BINARY="${BINARY_NAME}"
+
+FILE_COUNT="$(
+ find "${EXTRACT_DIR}" -type f -print |
+ wc -l |
+ tr -d ' '
+)"
+
+[ "${FILE_COUNT}" = "1" ] ||
+ fail "Installer archive is invalid: expected exactly one file."
+
+EXTRACTED_BINARY="${EXTRACT_DIR}/${EXPECTED_BINARY}"
+
+[ -f "${EXTRACTED_BINARY}" ] ||
+ fail "Installer archive is invalid: expected ${EXPECTED_BINARY}."
+
+# Reject nested directories or unexpected paths.
+DIRECTORY_COUNT="$(
+ find "${EXTRACT_DIR}" -type d -mindepth 1 -print |
+ wc -l |
+ tr -d ' '
+)"
+
+[ "${DIRECTORY_COUNT}" = "0" ] ||
+ fail "Installer archive is invalid: it contains nested directories."
+
+# Prepare installation directory
+
+if [ ! -d "${INSTALL_DIR}" ]; then
+ info "Creating ${INSTALL_DIR}..."
+
+ if mkdir -p "${INSTALL_DIR}" 2>/dev/null; then
+ :
+ elif command -v sudo >/dev/null 2>&1; then
+ sudo mkdir -p "${INSTALL_DIR}"
+ else
+ fail "Cannot create ${INSTALL_DIR} and sudo is not available."
+ fi
+fi
+
+# Install binary
+
+chmod 0755 "${EXTRACTED_BINARY}" ||
+ fail "Could not make ${BINARY_NAME} executable."
+
+DESTINATION="${INSTALL_DIR}/${BINARY_NAME}"
+
+if [ -w "${INSTALL_DIR}" ]; then
+ mv "${EXTRACTED_BINARY}" "${DESTINATION}" ||
+ fail "Could not install ${BINARY_NAME} to ${INSTALL_DIR}."
+else
+ if command -v sudo >/dev/null 2>&1; then
+ info "Administrator permission is required to install to ${INSTALL_DIR}."
+ sudo mv "${EXTRACTED_BINARY}" "${DESTINATION}" ||
+ fail "Could not install ${BINARY_NAME} to ${INSTALL_DIR}."
+ else
+ fail "Cannot write to ${INSTALL_DIR} and sudo is not available."
+ fi
+fi
+
+# Ensure the final installed file is executable.
+if [ ! -x "${DESTINATION}" ]; then
+ if command -v sudo >/dev/null 2>&1 && [ ! -w "${INSTALL_DIR}" ]; then
+ sudo chmod 0755 "${DESTINATION}"
+ else
+ chmod 0755 "${DESTINATION}"
+ fi
+fi
+
+# Verify installation
+
+if [ ! -x "${DESTINATION}" ]; then
+ fail "Installation completed but ${DESTINATION} is not executable."
+fi
+
+info "Installed ExplainThisRepo ${VERSION}."
+info "Location: ${DESTINATION}"
+
+# /usr/local/bin is normally already in PATH.
+# A shell process cannot modify the PATH of its parent shell, so
+# explicitly explain the only remaining user-side issue if needed.
+
+case ":${PATH}:" in
+ *":${INSTALL_DIR}:"*)
+ info "Run: ${BINARY_NAME}"
+ ;;
+
+ *)
+ echo
+ echo "The binary was installed successfully, but ${INSTALL_DIR} is not in your PATH."
+ echo "Add ${INSTALL_DIR} to your PATH, then run:"
+ echo " ${BINARY_NAME}"
+ ;;
+esac
\ No newline at end of file