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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curl-install-channel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": minor
---

Add a curl installer (`curl -fsSL https://hunk.dev/install.sh | sh`) with checksum verification, and teach `hunk update` to update curl installs.
2 changes: 1 addition & 1 deletion .dependency-cruiser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ module.exports = {
"core/bootstrap.ts composes the leaves: it names the changeset, the parsed input, the resolved preferences, and the detected theme mode to describe one launch. A module directory importing it back would invert that layering and rebuild the grab-bag cycle the 2026-08 phases dismantled. core/changeset/loaders.ts is the single exception — loadAppBootstrap assembles the value, so it names the shape it returns; its natural home is the app tier, and moving it there retires this exception.",
severity: "error",
from: {
path: "^src/core/(changeset|run|process|review|vcs|watch|patch|theme)/",
path: "^src/core/(changeset|run|process|install|review|vcs|watch|patch|theme)/",
pathNot: "^src/core/changeset/loaders\\.ts$",
},
to: { path: "^src/core/bootstrap\\.ts$" },
Expand Down
99 changes: 99 additions & 0 deletions .github/workflows/install-sh-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Runs the hunk.dev install script end to end against the newest published release.
#
# The unit suite (scripts/install-sh.test.ts) checks the script as text and exercises its
# functions in isolation; this workflow is the only place the real download, checksum, extract,
# and PATH flow runs against real release assets. It triggers on changes to the script itself,
# on a weekly schedule to catch release-asset drift the script did not cause, and on demand.
#
# The release version is resolved with an authenticated `gh` call and pinned via HUNK_VERSION
# instead of exercising the script's own anonymous latest-release lookup: shared runner IPs hit
# GitHub's unauthenticated API rate limits, and a rate-limited lookup would fail runs the script
# did nothing to deserve. The lookup's parsing is covered by the unit suite.
name: Install script E2E

on:
pull_request:
paths:
- install.sh
- .github/workflows/install-sh-e2e.yml
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
install-e2e:
name: Install script E2E (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
# dash serves /bin/sh on ubuntu and bash-as-sh on macos, so both POSIX modes get exercised.
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Resolve the newest published release
id: release
env:
GH_TOKEN: ${{ github.token }}
run: |
tag="$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName --jq .tagName)"
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"

- name: Install into a sandbox home
env:
HUNK_VERSION: ${{ steps.release.outputs.version }}
run: |
set -eu
sandbox="$(mktemp -d)"
github_path="${sandbox}/github_path"
: >"$github_path"

HOME="$sandbox" GITHUB_PATH="$github_path" sh install.sh

binary="${sandbox}/.hunk/bin/hunk"
[ -x "$binary" ] || { echo "error: no executable at $binary" >&2; exit 1; }
"$binary" --version | grep -F "$HUNK_VERSION"
skill="$("$binary" skill path hunk-review)"
[ -f "$skill" ] || { echo "error: skill path $skill does not exist" >&2; exit 1; }
grep -F "${sandbox}/.hunk/bin" "$github_path"

- name: Re-run is idempotent
env:
HUNK_VERSION: ${{ steps.release.outputs.version }}
run: |
set -eu
sandbox="$(mktemp -d)"
HOME="$sandbox" GITHUB_PATH="${sandbox}/github_path" sh install.sh
output="$(HOME="$sandbox" GITHUB_PATH="${sandbox}/github_path" sh install.sh)"
printf '%s\n' "$output" | grep -F "already installed"

- name: Custom directory with spaces installs and resolves skills
env:
HUNK_VERSION: ${{ steps.release.outputs.version }}
run: |
set -eu
sandbox="$(mktemp -d)"
custom="${sandbox}/hunk tools"

HOME="$sandbox" HUNK_INSTALL_DIR="$custom" sh install.sh --no-modify-path

binary="${custom}/hunk"
[ -x "$binary" ] || { echo "error: no executable at $binary" >&2; exit 1; }
"$binary" --version | grep -F "$HUNK_VERSION"
skill="$("$binary" skill path hunk-review)"
[ -f "$skill" ] || { echo "error: skill path $skill does not exist" >&2; exit 1; }

- name: Unknown version fails loudly
run: |
set -eu
sandbox="$(mktemp -d)"
if HOME="$sandbox" HUNK_VERSION="0.0.999" sh install.sh --no-modify-path 2>"${sandbox}/err"; then
echo "error: installing a nonexistent version should have failed" >&2
exit 1
fi
grep -F "Could not download" "${sandbox}/err"
15 changes: 12 additions & 3 deletions .github/workflows/release-prebuilt-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,18 @@ jobs:
done < <(find dist/release/artifacts -mindepth 1 -maxdepth 1 -type d -name 'hunkdiff-*' -print0 | sort -z)
find dist/release/github -maxdepth 1 -type f | sort

# Attest the archives before they are uploaded so the provenance covers
# exactly the bytes published as release assets. The subject glob is kept
# identical to the upload glob below so nothing can ship unattested.
# One checksum manifest covering every archive, so `install.sh` can verify the
# download it just made. Names are written bare, without directories, so the file
# is checkable from whatever directory the installer extracts into.
- name: Write release checksums
run: |
cd dist/release/github
sha256sum hunkdiff-*.tar.gz > SHA256SUMS
cat SHA256SUMS

# Attest the archives and their checksum manifest before they are uploaded so the
# provenance covers exactly the bytes published as release assets. The subject glob
# is kept identical to the upload glob below so nothing can ship unattested.
# mise/aqua verifies these through GitHub Artifact Attestations on install.
- name: Attest release archives
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ Hunk is a review-first terminal diff viewer for agent-authored changesets, built
npm i -g hunkdiff
```

Or with the install script on macOS and Linux, which downloads the prebuilt binary, verifies its checksum, and installs into `~/.hunk`:

```bash
curl -fsSL https://hunk.dev/install.sh | sh
```

Or with Homebrew:

```bash
Expand All @@ -53,7 +59,7 @@ mise use -g hunk
Requirements:

- macOS, Linux, or Windows
- Node.js 18+ for the npm install; Homebrew, mise, and Nix ship a standalone binary
- Node.js 18+ for the npm install; the install script, Homebrew, mise, and Nix ship a standalone binary
- Git recommended for most workflows

> Nix users can use the `default` package exported in `flake.nix` instead. See [nix/README.md](./nix/README.md) for details.
Expand Down
9 changes: 9 additions & 0 deletions docs/module-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ shape it assembles; that function is composition living in the domain tier, and
`src/core/` root now holds `bootstrap.ts`, `reviewDigest.ts`, and `liveComments.ts` beside the
eight module directories.

Phase 5 (2026-08-18) grouped **how this binary was installed and how it gets replaced** into
`core/install/`: install-source detection (`installSource` — which channel owns the executable),
per-channel release lookup (`latestRelease`), and the `hunk update` execution (`selfUpdate`).
These arrived with the self-update feature as `core/process/` files because the startup update
notice lived there, but they are one feature family about the install lifecycle, not about the
process a run lives in. `process/updateNotice` stays where phase 3 put it — it is a
startup-notice producer built on `appStateFile` — and consumes `core/install` for detection and
release lookup. The move is path-only; no exported symbol changed.

## Snapshot (2026-08-17, v0.19.0)

331 production modules, 1322 internal edges, **zero boundary violations and zero import
Expand Down
Loading
Loading