diff --git a/README.md b/README.md
index 7a68339..2badde1 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,12 @@ Get protected in seconds.
### 1. Install
+**MacOS / Linux (Install Script)**
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/safedep/pmg/main/install.sh | sh
+```
+
**MacOS / Linux (Homebrew)**
```bash
@@ -66,6 +72,8 @@ brew install safedep/tap/pmg
npm install -g @safedep/pmg
```
+> **Note:** NPM-based installs can be fragile when Node.js is managed by version managers like [`mise`](https://mise.jdx.dev/) or [`asdf`](https://asdf-vm.com/), since the global `npm` bin path changes with the active Node version. Prefer the install script or Homebrew in those setups.
+
> See [Installation](#installation) for additional methods.
### 2. Setup
@@ -140,6 +148,17 @@ PMG supports the tools you already use:
## Installation
+
+Install Script (MacOS/Linux)
+
+Downloads the latest release from GitHub, verifies its SHA-256 checksum, and installs to `$HOME/.local/bin` (if on `PATH`) or `/usr/local/bin`.
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/safedep/pmg/main/install.sh | sh
+```
+
+
+
Homebrew (MacOS/Linux)
@@ -157,6 +176,8 @@ brew install safedep/tap/pmg
npm install -g @safedep/pmg
```
+> **Note:** NPM-based installs can be fragile when Node.js is managed by version managers like [`mise`](https://mise.jdx.dev/) or [`asdf`](https://asdf-vm.com/). The global `npm` bin path changes with the active Node version, so switching versions can leave `pmg` unavailable on `PATH` (or pointing to an old install). For these setups, prefer the install script or Homebrew.
+
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..8054171
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,148 @@
+#!/bin/sh
+set -eu
+
+do_install() {
+ REPO="safedep/pmg"
+ BINARY="pmg"
+
+ # Prefer $HOME/.local/bin if it exists and is in PATH.
+ INSTALL_DIR="/usr/local/bin"
+ if [ -n "${HOME:-}" ]; then
+ local_bin="$HOME/.local/bin"
+ case ":${PATH}:" in
+ *":${local_bin}:"*)
+ INSTALL_DIR="$local_bin"
+ mkdir -p "$INSTALL_DIR"
+ ;;
+ esac
+ fi
+
+ # Detect OS and architecture.
+ OS="$(uname -s)"
+ ARCH="$(uname -m)"
+
+ case "$OS" in
+ Linux) os="Linux" ;;
+ Darwin) os="Darwin" ;;
+ MINGW* | MSYS* | CYGWIN*) os="Windows" ;;
+ *)
+ echo "Error: unsupported operating system: $OS" >&2
+ exit 1
+ ;;
+ esac
+
+ case "$ARCH" in
+ x86_64 | amd64) arch="x86_64" ;;
+ aarch64 | arm64) arch="arm64" ;;
+ i386 | i686) arch="i386" ;;
+ *)
+ echo "Error: unsupported architecture: $ARCH" >&2
+ exit 1
+ ;;
+ esac
+
+ # macOS ships a universal binary.
+ if [ "$os" = "Darwin" ]; then
+ arch="all"
+ fi
+
+ if [ "$os" = "Windows" ]; then
+ BINARY="pmg.exe"
+ fi
+
+ # Verify install directory exists.
+ if [ ! -d "$INSTALL_DIR" ]; then
+ echo "Error: install directory ${INSTALL_DIR} does not exist" >&2
+ echo "Create it with: sudo mkdir -p ${INSTALL_DIR}" >&2
+ exit 1
+ fi
+
+ # Fetch latest release tag via redirect (avoids JSON parsing).
+ echo "Fetching latest release..."
+ tag=$(curl -fsSI -o /dev/null -w '%{redirect_url}' "https://github.com/${REPO}/releases/latest" | sed 's|.*/||')
+ if [ -z "$tag" ]; then
+ echo "Error: could not determine latest release" >&2
+ exit 1
+ fi
+ echo "Latest release: $tag"
+
+ # Build download URL.
+ # Assets follow the pattern: pmg_{OS}_{arch}.{tar.gz|zip}
+ if [ "$os" = "Windows" ]; then
+ asset="pmg_${os}_${arch}.zip"
+ else
+ asset="pmg_${os}_${arch}.tar.gz"
+ fi
+ url="https://github.com/${REPO}/releases/download/${tag}/${asset}"
+
+ # Download archive and checksums.
+ echo "Downloading ${asset}..."
+ tmpdir=$(mktemp -d)
+ trap 'rm -rf "$tmpdir"' EXIT
+ checksums_url="https://github.com/${REPO}/releases/download/${tag}/checksums.txt"
+ if ! curl -fsSL -o "${tmpdir}/${asset}" "$url"; then
+ echo "Error: archive not available for ${os}/${arch}" >&2
+ echo "Check available assets at https://github.com/${REPO}/releases/tag/${tag}" >&2
+ exit 1
+ fi
+ if ! curl -fsSL -o "${tmpdir}/checksums.txt" "$checksums_url"; then
+ echo "Error: could not download checksums.txt from ${checksums_url}" >&2
+ exit 1
+ fi
+
+ # Verify SHA-256 checksum.
+ expected=$(grep " ${asset}$" "${tmpdir}/checksums.txt" | cut -d' ' -f1)
+ if [ -z "$expected" ]; then
+ echo "Error: no checksum entry found for ${asset} in checksums.txt" >&2
+ exit 1
+ fi
+ if command -v sha256sum >/dev/null 2>&1; then
+ actual=$(sha256sum "${tmpdir}/${asset}" | cut -d' ' -f1)
+ elif command -v shasum >/dev/null 2>&1; then
+ actual=$(shasum -a 256 "${tmpdir}/${asset}" | cut -d' ' -f1)
+ else
+ echo "Error: neither sha256sum nor shasum found; cannot verify download" >&2
+ echo " Install one of them and re-run, or download manually from:" >&2
+ echo " https://github.com/${REPO}/releases/tag/${tag}" >&2
+ exit 1
+ fi
+ if [ "$actual" != "$expected" ]; then
+ echo "Error: checksum mismatch for ${asset}" >&2
+ echo " expected: $expected" >&2
+ echo " actual: $actual" >&2
+ exit 1
+ fi
+ echo "Checksum verified."
+
+ if [ "$os" = "Windows" ]; then
+ unzip -q -o "${tmpdir}/${asset}" "${BINARY}" -d "${tmpdir}"
+ else
+ tar -xzf "${tmpdir}/${asset}" -C "${tmpdir}" "${BINARY}"
+ fi
+
+ if [ ! -f "${tmpdir}/${BINARY}" ]; then
+ echo "Error: ${BINARY} not found in archive" >&2
+ exit 1
+ fi
+
+ # Install.
+ if [ -w "$INSTALL_DIR" ]; then
+ install -m 755 "${tmpdir}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
+ else
+ echo "Installing to ${INSTALL_DIR} (requires sudo)..."
+ sudo install -m 755 "${tmpdir}/${BINARY}" "${INSTALL_DIR}/${BINARY}"
+ fi
+
+ echo "Installed pmg ${tag} to ${INSTALL_DIR}/${BINARY}"
+
+ # Verify the install directory is in PATH.
+ case ":${PATH}:" in
+ *":${INSTALL_DIR}:"*) ;;
+ *)
+ echo "Warning: ${INSTALL_DIR} is not in your PATH. Add it with:" >&2
+ echo " export PATH=\"${INSTALL_DIR}:\$PATH\"" >&2
+ ;;
+ esac
+}
+
+do_install