From edeb05420cd116054112b126dad6e8466564bf55 Mon Sep 17 00:00:00 2001 From: Justin Giancola Date: Tue, 11 Aug 2026 15:15:10 -0400 Subject: [PATCH 1/3] fix(update-notice): suppress startup update notice for mise installs Hunk now ships as an omarchy default tool, installed via `omarchy-mise-install aqua:modem-dev/hunk hunk`. That wrapper runs `mise use -g` before exec'ing the binary, so mise-managed sessions are already on the newest release by the time Hunk starts. Previously those installs fell through to the "npm" install source and were told to run `npm i -g hunkdiff` -- wrong for how they installed, and redundant with an upgrade that just happened. Detect mise from adjacent `mise/installs` path segments (the one signal that survives `mise x`, which sets none of mise's shell env vars) and suppress the notice via a named self-updating-source policy rather than swapping in a mise command. The suppression deliberately sits after the skill-refresh notice, so mise-managed sessions still receive a one-time refresh notice. --- .changeset/mise-update-notice.md | 5 ++ src/core/updateNotice.test.ts | 79 ++++++++++++++++++++++++++++++++ src/core/updateNotice.ts | 63 +++++++++++++++++++++++-- 3 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 .changeset/mise-update-notice.md diff --git a/.changeset/mise-update-notice.md b/.changeset/mise-update-notice.md new file mode 100644 index 000000000..22c09baf7 --- /dev/null +++ b/.changeset/mise-update-notice.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Recognize mise-managed installs and skip the startup update notice for them, since mise already keeps Hunk up to date. diff --git a/src/core/updateNotice.test.ts b/src/core/updateNotice.test.ts index 57e5b6eb5..11b53483d 100644 --- a/src/core/updateNotice.test.ts +++ b/src/core/updateNotice.test.ts @@ -160,6 +160,85 @@ describe("startup update notice", () => { }); }); + test("suppresses update notices for mise-managed installs", async () => { + await withTempStatePath(async (statePath) => { + await expect( + resolveStartupUpdateNotice({ + env: { HUNK_INSTALL_SOURCE: "mise" }, + fetchImpl: async () => { + throw new Error("should not fetch for mise installs"); + }, + resolveInstalledVersion: () => "0.7.0", + statePath, + }), + ).resolves.toBeNull(); + }); + }); + + test("detects unmarked mise installs from their mise install path", async () => { + await withTempStatePath(async (statePath) => { + await expect( + resolveStartupUpdateNotice({ + env: {}, + fetchImpl: async () => { + throw new Error("should not fetch for mise installs"); + }, + resolveExecutablePath: () => + "/home/user/.local/share/mise/installs/aqua-modem-dev-hunk/0.7.0/hunk", + resolveInstalledVersion: () => "0.7.0", + statePath, + }), + ).resolves.toBeNull(); + }); + }); + + test("detects unmarked mise installs from Windows-style install paths", async () => { + await withTempStatePath(async (statePath) => { + await expect( + resolveStartupUpdateNotice({ + env: {}, + fetchImpl: async () => { + throw new Error("should not fetch for mise installs"); + }, + resolveExecutablePath: () => + "C:\\Users\\user\\AppData\\Local\\mise\\installs\\aqua-modem-dev-hunk\\0.7.0\\hunk.exe", + resolveInstalledVersion: () => "0.7.0", + statePath, + }), + ).resolves.toBeNull(); + }); + }); + + test("never surfaces beta or latest notices for a resolved mise install source", async () => { + await withTempStatePath(async (statePath) => { + await expect( + resolveStartupUpdateNotice({ + fetchImpl: async () => createDistTagsResponse({ latest: "0.7.0", beta: "0.8.0-beta.1" }), + resolveInstalledVersion: () => "0.7.0", + resolveInstallSource: () => "mise", + statePath, + }), + ).resolves.toBeNull(); + }); + }); + + test("keeps npm notices for paths that only mention mise outside an install directory", async () => { + await withTempStatePath(async (statePath) => { + await expect( + resolveStartupUpdateNotice({ + env: {}, + fetchImpl: async () => createDistTagsResponse({ latest: "0.7.1" }), + resolveExecutablePath: () => "/home/mise/projects/hunk/node_modules/.bin/hunk", + resolveInstalledVersion: () => "0.7.0", + statePath, + }), + ).resolves.toEqual({ + key: "latest:0.7.1", + message: "Update available: 0.7.1 (latest) • npm i -g hunkdiff", + }); + }); + }); + test("returns null when already up to date", async () => { await withTempStatePath(async (statePath) => { await expect( diff --git a/src/core/updateNotice.ts b/src/core/updateNotice.ts index 4152ab5e5..62d90ee63 100644 --- a/src/core/updateNotice.ts +++ b/src/core/updateNotice.ts @@ -1,3 +1,4 @@ +import { posix, win32 } from "node:path"; import { readHunkStateRecord, updateHunkStateRecord } from "./hunkState"; import { resolveHunkStatePath } from "./paths"; import type { StartupNotice } from "./startupNotice"; @@ -17,7 +18,17 @@ interface PersistedStartupState { } export type UpdateChannel = "latest" | "beta"; -export type InstallSource = "npm" | "homebrew" | "nix"; +export type InstallSource = "npm" | "homebrew" | "nix" | "mise"; + +/** + * Install sources that upgrade Hunk on their own, so Hunk never surfaces an update notice for them. + * + * mise owns its tool versions: omarchy's `hunk` wrapper runs `mise use -g aqua:modem-dev/hunk` + * before exec'ing the binary, so the newest release is already installed by the time this session + * starts. A notice there would ask the user to fix something mise just fixed, so suppress rather + * than swap in a mise-flavored update command. + */ +const SELF_UPDATING_INSTALL_SOURCES: readonly InstallSource[] = ["mise"]; type FetchImpl = (input: RequestInfo | URL, init?: RequestInit) => Promise; @@ -68,20 +79,56 @@ function isNewerVersion(current: string, candidate: string) { } } +/** Split one filesystem path into segments, tolerating either platform's separator. */ +function splitPathSegments(candidatePath: string) { + return candidatePath + .split(win32.sep) + .flatMap((segment) => segment.split(posix.sep)) + .filter((segment) => segment.length > 0); +} + +/** + * Return whether this executable lives inside a mise-managed install directory. + * + * mise lays every backend out as `/mise/installs///` on all + * platforms, so the adjacent `mise/installs` segments are the one signal that survives `mise x` + * (the omarchy wrapper's launch path, which sets none of mise's shell env vars) as well as shims + * and activated shells. + */ +function isMiseManagedExecutablePath(executablePath: string) { + const segments = splitPathSegments(executablePath); + return segments.some( + (segment, index) => segment === "mise" && segments[index + 1] === "installs", + ); +} + /** Resolve which package manager installed this binary, defaulting to the npm package path. */ function resolveInstallSourceFromRuntime( env: NodeJS.ProcessEnv = process.env, executablePath = process.execPath, ): InstallSource { const installSource = env[INSTALL_SOURCE_ENV]; - if (installSource === "homebrew" || installSource === "nix") { + if (installSource === "homebrew" || installSource === "nix" || installSource === "mise") { return installSource; } - return executablePath.startsWith("/nix/store/") ? "nix" : "npm"; + if (executablePath.startsWith("/nix/store/")) { + return "nix"; + } + + return isMiseManagedExecutablePath(executablePath) ? "mise" : "npm"; +} + +/** Return whether the install source manages its own upgrades and needs no update notice. */ +function managesOwnUpdates(installSource: InstallSource) { + return SELF_UPDATING_INSTALL_SOURCES.includes(installSource); } -/** Build the install-aware update instruction shown for one release channel. */ +/** + * Build the install-aware update instruction shown for one release channel. + * + * Self-updating sources never reach here; they are filtered out before the dist-tag lookup. + */ function updateInstructionForChannel(channel: UpdateChannel, installSource: InstallSource) { if (installSource === "homebrew") { return "brew update && brew upgrade hunk"; @@ -259,6 +306,12 @@ export async function resolveStartupUpdateNotice( const resolveInstallSource = deps.resolveInstallSource ?? (() => resolveInstallSourceFromRuntime(env, deps.resolveExecutablePath?.())); + const installSource = resolveInstallSource(); + // Resolved before fetching so self-updating installs skip the dist-tag request entirely. + if (managesOwnUpdates(installSource)) { + return null; + } + const { signal, dispose } = createFetchTimeoutSignal(fetchTimeoutMs); try { @@ -268,7 +321,7 @@ export async function resolveStartupUpdateNotice( } const parsedPayload = parseDistTags(await response.json()); - return selectUpdateNotice(resolveInstalledVersion(), parsedPayload, resolveInstallSource()); + return selectUpdateNotice(resolveInstalledVersion(), parsedPayload, installSource); } catch { return null; } finally { From 2e004c6e04b0a477d61ef4763303d1075b9c1c4e Mon Sep 17 00:00:00 2001 From: Justin Giancola Date: Tue, 11 Aug 2026 15:15:16 -0400 Subject: [PATCH 2/3] build(release): attest release archives with build provenance mise verifies aqua-installed tools by default (`aqua.github_attestations`, `aqua.cosign`, and `aqua.minisign` all default to true), but Hunk publishes no verification material at all, so every mise/aqua install is unverified. Attest the archives with `actions/attest-build-provenance` before upload. The subject glob deliberately mirrors the upload glob rather than matching `*.tar.gz`, so the invariant is "nothing leaves that directory unattested" even if the asset set changes later. This is the producing half. A follow-up PR to aqua-registry adds the `github_artifact_attestations:` block, and must wait for a real attested release so the signer workflow identity is read off a live attestation instead of hand-written. --- .changeset/attested-release-archives.md | 5 +++++ .github/workflows/release-prebuilt-npm.yml | 13 +++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 .changeset/attested-release-archives.md diff --git a/.changeset/attested-release-archives.md b/.changeset/attested-release-archives.md new file mode 100644 index 000000000..e77757811 --- /dev/null +++ b/.changeset/attested-release-archives.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Publish GitHub build provenance attestations for the release archives so installs can be cryptographically verified. diff --git a/.github/workflows/release-prebuilt-npm.yml b/.github/workflows/release-prebuilt-npm.yml index e87a619ef..97a93223c 100644 --- a/.github/workflows/release-prebuilt-npm.yml +++ b/.github/workflows/release-prebuilt-npm.yml @@ -236,6 +236,10 @@ jobs: if: github.event_name == 'push' permissions: contents: write + # Required by actions/attest-build-provenance: `id-token` mints the Sigstore + # OIDC token and `attestations` writes the bundle to GitHub's attestation store. + id-token: write + attestations: write steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -275,6 +279,15 @@ 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. + # mise/aqua verifies these through GitHub Artifact Attestations on install. + - name: Attest release archives + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: dist/release/github/* + - name: Create or update GitHub release env: GH_TOKEN: ${{ github.token }} From 517b75ab34e7570fdeea82d5099380bfa14e091c Mon Sep 17 00:00:00 2001 From: Justin Giancola Date: Tue, 11 Aug 2026 15:15:23 -0400 Subject: [PATCH 3/3] docs: document the mise install path `mise use -g hunk` has worked since community contributors added Hunk to the mise and aqua registries, but nothing in the repo said so -- and Hunk now reaches users primarily through that path as an omarchy default tool. Scope the "Node.js 18+" requirement to the npm install while doing so; it never applied to the Homebrew, Nix, or mise paths, which ship a self-contained binary. Record the release-verification command too. mise bakes an aqua registry snapshot into each of its releases and caches registry sources for a week, so a naive post-publish check can pass or fail for reasons unrelated to our release; `MISE_AQUA_BAKED_REGISTRY=false` is what makes it meaningful. --- .changeset/document-mise-install.md | 5 +++++ AGENTS.md | 1 + README.md | 10 +++++++++- website/src/content/docs/docs/start/install.md | 17 ++++++++++++++--- website/src/pages/index.astro | 1 + 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 .changeset/document-mise-install.md diff --git a/.changeset/document-mise-install.md b/.changeset/document-mise-install.md new file mode 100644 index 000000000..fbb1d2f99 --- /dev/null +++ b/.changeset/document-mise-install.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Document installing Hunk with mise, and note that Hunk ships as a default Omarchy tool. diff --git a/AGENTS.md b/AGENTS.md index 44e5f8b32..f15dcebd9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -155,6 +155,7 @@ CLI input - Do not rely blindly on autogenerated GitHub release notes. After publishing, verify the release body and edit it if needed. - Prefer `gh release create/edit --notes-file` for multi-line release notes so the exact body is reviewed before posting. - After publishing, verify npm packages and GitHub release assets point at the new version. For Homebrew, Hunk is distributed through `Homebrew/homebrew-core`; do not open manual simple version-bump PRs yourself. Let Homebrew Autobump create the `hunk ` PR, then verify it merges and `brew install hunk` resolves to the new version. Only use `brew bump-formula-pr hunk --version ` if Homebrew maintainers request a manual bump or Autobump stalls unexpectedly. +- For mise, verify with `MISE_AQUA_BAKED_REGISTRY=false mise latest hunk`. mise resolves Hunk through the community `aqua:modem-dev/hunk` entry, bakes an aqua registry snapshot into each of its own releases (`aqua.baked_registry`, default true), and caches downloaded registry sources for a week (`aqua.registry_cache_ttl`), so a default check can report stale registry data unrelated to our release; `mise cache clear` forces a refresh. - For patch releases and backports, list only changes actually present between the previous tag and the new tag on that release branch. - Prefer concise, user-visible entries over internal refactors unless the refactor changes user-visible behavior. - Keep each changeset summary to one concise user-facing sentence; put implementation detail in the PR or supporting docs. diff --git a/README.md b/README.md index 72efd1e60..644197ea4 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,22 @@ brew install hunk > [!NOTE] > If you previously installed hunk via `modem-dev/tap`, be sure to uninstall it first with `brew uninstall modem-dev/tap/hunk`. +Or with [mise](https://mise.jdx.dev) (macOS and Linux): + +```bash +mise use -g hunk +``` + Requirements: -- Node.js 18+ - macOS, Linux, or Windows +- Node.js 18+ for the npm install; 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. +> Hunk also ships as a default tool in [Omarchy](https://omarchy.org), installed through mise. + ## Quick start ```bash diff --git a/website/src/content/docs/docs/start/install.md b/website/src/content/docs/docs/start/install.md index 135665c48..8feba6993 100644 --- a/website/src/content/docs/docs/start/install.md +++ b/website/src/content/docs/docs/start/install.md @@ -1,9 +1,9 @@ --- title: Install -description: Install Hunk with npm, Homebrew, or Nix and verify the CLI. +description: Install Hunk with npm, Homebrew, mise, or Nix and verify the CLI. --- -Hunk runs on macOS, Linux, and Windows. npm installs require Node.js 18 or newer; Homebrew and Nix installs are self-contained binaries. Git is recommended for the most common review workflows. +Hunk runs on macOS, Linux, and Windows. npm installs require Node.js 18 or newer; Homebrew, mise, and Nix installs are self-contained binaries. Git is recommended for the most common review workflows. ## npm @@ -30,6 +30,17 @@ brew uninstall modem-dev/tap/hunk brew install hunk ``` +## mise + +[mise](https://mise.jdx.dev) knows Hunk by the short name `hunk` (alias `hunkdiff`) and installs the prebuilt binary on macOS and Linux: + +```bash +mise use -g hunk +hunk --version +``` + +Hunk also ships as a default tool in [Omarchy](https://omarchy.org), which installs it through mise. + ## Nix The repository exports a `default` package from `flake.nix`. From a clone of Hunk: @@ -47,6 +58,6 @@ See the repository's `nix/README.md` for Home Manager and development-shell deta hunk --help ``` -You should see `Usage: hunk [options]`. If the shell cannot find Hunk, ensure your global npm or Homebrew binary directory is on `PATH`, then open a new shell. +You should see `Usage: hunk [options]`. If the shell cannot find Hunk, ensure your global npm, Homebrew, or mise binary directory is on `PATH`, then open a new shell. Next, [review your first working tree](/docs/start/quick-start/). diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index a463a229c..5cbc577b9 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -169,6 +169,7 @@ const formattedStars =
  • npmnpm i -g hunkdiff
  • Homebrewbrew install hunk
  • +
  • misemise use -g hunk
  • Nixnix run github:modem-dev/hunk