diff --git a/.github/release-notes.md b/.github/release-notes.md index 37c4442..ae8749e 100644 --- a/.github/release-notes.md +++ b/.github/release-notes.md @@ -1,7 +1,5 @@ ## Install -Installs the binaries to `~/.local/bin`. - ```bash -curl -fsSL https://github.com/rubas/kagi/releases/download/__VERSION__/install.sh | sh -s -- __VERSION__ +mise use -g github:rubas/kagi@__VERSION__ ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edec664..3afad68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,6 @@ jobs: # task and zizmor come with the incus runner image. - run: task fmt:check - run: task lint - - run: shellcheck install.sh - run: task test - run: task test:release-check - run: task deps:machete diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 266e488..1f97b20 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -150,39 +150,13 @@ jobs: name: ${{ matrix.archive }} path: ${{ matrix.archive }} - installer: - name: Installer - needs: [prepare, tag] - if: ${{ !cancelled() && !failure() && needs.prepare.outputs.should_release == 'true' }} - runs-on: ubuntu-latest - permissions: - contents: read # checkout source to upload install.sh - id-token: write # sign build provenance attestations - attestations: write # store build provenance attestations - concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-installer - cancel-in-progress: false - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ needs.prepare.outputs.ref }} - persist-credentials: false - - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 - with: - subject-path: install.sh - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: install.sh - path: install.sh - release: name: Release runs-on: ubuntu-latest - needs: [prepare, build, installer] + needs: [prepare, build] if: ${{ !cancelled() && !failure() && needs.prepare.outputs.should_release == 'true' }} permissions: contents: write # publish the GitHub release - attestations: read # smoke test verifies the attestations of the built archive and install.sh concurrency: group: ${{ github.workflow }}-${{ github.ref }}-release cancel-in-progress: false @@ -196,44 +170,6 @@ jobs: with: path: dist merge-multiple: true - - name: Smoke-test install.sh against built artifacts - shell: bash - env: - TAG: ${{ needs.prepare.outputs.tag }} - GH_TOKEN: ${{ github.token }} - run: | - gh attestation verify dist/install.sh --repo "$GITHUB_REPOSITORY" - home="$(mktemp -d)" - dist="file://$PWD/dist" - kagi_install() { HOME="$home" KAGI_INSTALL_BASE_URL="$dist" sh dist/install.sh "$@"; } - snapshot() { find "$home" -printf '%i %T@ %p %l\n' | sort; } - - # An upgrade replaces the installed version. - mkdir -p "$home/.local/bin" - printf '#!/bin/sh\necho kagi-search 0.5.4\n' > "$home/.local/bin/kagi-search" - chmod +x "$home/.local/bin/kagi-search" - status=0 - kagi_install --check "$TAG" || status=$? - test "$status" -eq 100 - - kagi_install "$TAG" - test -x "$home/.local/bin/kagi-search" - test -x "$home/.local/bin/kagi-maps" - test -x "$home/.local/bin/kagi-summarize" - - # A second run finds the current version and changes nothing. - before="$(snapshot)" - kagi_install "$TAG" | grep -Fx "kagi ${TAG#v} is current" - test "$before" = "$(snapshot)" - kagi_install --check "$TAG" - - # A first install. - home="$(mktemp -d)" - status=0 - kagi_install --check "$TAG" || status=$? - test "$status" -eq 100 - kagi_install "$TAG" - test -x "$home/.local/bin/kagi-search" - name: Write release notes shell: bash env: @@ -249,4 +185,3 @@ jobs: files: | dist/kagi-linux-x86_64.tar.gz dist/kagi-macos-aarch64.tar.gz - dist/install.sh diff --git a/AGENTS.md b/AGENTS.md index a1a39f4..b3da694 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,12 +5,6 @@ covers install, the token, and usage. ## Gates -- `task ci` runs `task check`, the release-profile check, `nix build`, cargo-machete, and - cargo-deny. GitHub Actions runs all of it except `task test:nix`. Run `task ci` yourself after - you touch `flake.nix`, `flake.lock`, or `Cargo.toml`, because nothing else builds the Nix - package. This includes a lock-only input refresh. -- `task lint`, and with it `task check` and `task ci`, runs `zizmor`. The `nix develop` shell does - not include it, so put `zizmor` on `PATH` first. - `task test:live` calls the real Kagi service. Run it when you change the request path or a parser: `src/cli.rs`, `src/client.rs`, or `src/parse.rs`. It needs a session token (see `README.md`) and fails without one. @@ -21,9 +15,10 @@ covers install, the token, and usage. - Never hardcode a session token. - `--sort` means two things. `kagi-search` sends it to Kagi as the `order` parameter. `kagi-maps` gets the whole result page, sorts it locally, then cuts it to `--limit`. -- `install.sh` is the one installer. `task install` and the release smoke test run it on local - archives. `task install` packs the local build like a release archive, so it runs only on the - two release platforms. +- Our machines install the CLIs with mise (`github:rubas/kagi`). mise picks the release archive + by the OS and architecture in its name, `kagi-linux-x86_64.tar.gz` or + `kagi-macos-aarch64.tar.gz`, and finds the binaries in its `bin/` dir. A change to an archive + name or layout breaks the install on every machine. - The repo ships no agent skill. The `search` skill in rubas/dotfiles covers these CLIs. - A version bump is the release trigger. On each push to `main`, `release.yml` reads `version` from `Cargo.toml`. When the tag `v` does not exist, it tags and publishes. The tag @@ -39,9 +34,5 @@ covers install, the token, and usage. ## Pitfalls -- `flake.nix` repeats the package version as a literal. Bump it in the same commit as - `Cargo.toml`, or `nix build` makes a package with the old version. -- `install.sh` runs under `sh` on Linux with GNU tools and on macOS with BSD tools. Use POSIX sh - and only flags that both sets have: no `realpath --relative-to`, no `ln -T`. - `ci.yml` runs its checks only on a pull request from a branch of this repo, so a fork PR never reaches the incus runners. A fork PR shows no checks. That is the gate, not a broken run. diff --git a/CHANGELOG.md b/CHANGELOG.md index e5064b2..eb0d785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project are documented in this file. The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Removed + +- `install.sh`. Releases no longer ship it. Install with mise instead: + `mise use -g github:rubas/kagi`. The release archives keep their names and + their attestation. +- The Nix flake, with its package, dev shell, and Home Manager module. + ## [0.6.1] - 2026-09-30 ### Removed diff --git a/README.md b/README.md index e60f61c..089fdbb 100644 --- a/README.md +++ b/README.md @@ -14,82 +14,17 @@ Kagi account and its session token. ## Install -### From a GitHub release +Releases ship Linux x86_64 and macOS aarch64 builds. Install them with [mise](https://mise.jdx.dev), +which verifies the GitHub attestation of the release archive: ```bash -curl -fsSL https://github.com/rubas/kagi/releases/latest/download/install.sh | sh +mise use -g github:rubas/kagi ``` -The installer puts `kagi-search`, `kagi-maps`, and `kagi-summarize` in `~/.local/bin`. - -Run the same command again to update. When `~/.local/bin/kagi-search --version` already shows the -target version, the installer says so and changes nothing. - -Put a release tag or an option after `sh -s --`: +Without mise, build from source: ```bash -# Install a given release. -curl -fsSL https://github.com/rubas/kagi/releases/latest/download/install.sh | sh -s -- v0.6.1 -# Show the installed and the latest version, and install nothing. -curl -fsSL https://github.com/rubas/kagi/releases/latest/download/install.sh | sh -s -- --check -``` - -- `--check` exits 0 when the installed version matches the target and 100 when kagi is not - installed or has a different version. -- `--force` installs again when the version already matches. - -Supported platforms: Linux x86_64 and macOS aarch64. - -When the GitHub CLI (`gh`) is available, the installer verifies the build provenance attestation of -the release archive before it changes a file. Without `gh`, it warns and continues. The Nix flake -below is the fully verifiable path: `flake.lock` pins every input by hash. - -The release also attests `install.sh`. To verify the installer before you run it: - -```bash -curl -fsSLO https://github.com/rubas/kagi/releases/latest/download/install.sh && - gh attestation verify install.sh --repo rubas/kagi && - KAGI_INSTALL_VERIFY=require sh install.sh -``` - -| Variable | Effect | -| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| `KAGI_INSTALL_VERIFY` | `auto` (default) verifies when `gh` is available. `require` fails without `gh`. `skip` does not verify. | -| `KAGI_INSTALL_BASE_URL` | Downloads the archive from this URL instead of the GitHub release, for example `file:///tmp/kagi`. Needs a version tag. | -| `KAGI_INSTALL_VERSION` | The version tag when no argument gives one. | -| `KAGI_INSTALL_REPO` | The GitHub repository, `rubas/kagi` by default. | - -### From source - -```bash -cargo install --git https://github.com/rubas/kagi.git -``` - -### With Nix flakes - -Install the CLIs directly: - -```bash -nix profile install github:rubas/kagi -``` - -Or enable the Home Manager module to install the CLIs: - -```nix -{ - inputs.kagi.url = "github:rubas/kagi"; - - outputs = { kagi, ... }: { - homeConfigurations.example = home-manager.lib.homeManagerConfiguration { - modules = [ - kagi.homeManagerModules.default - { - programs.kagi.enable = true; - } - ]; - }; - }; -} +cargo install --git https://github.com/rubas/kagi ``` ## Authentication @@ -153,12 +88,11 @@ kagi-summarize 'https://www.rust-lang.org/learn' --lang DE --json ## Development ```bash -nix develop task check ``` -`task lint` also runs [zizmor](https://github.com/zizmorcore/zizmor) over the workflows. The dev -shell does not include it, so install `zizmor` yourself or that step fails. +`task lint` also runs [zizmor](https://github.com/zizmorcore/zizmor) over the workflows, so install +`zizmor` first. ## License diff --git a/Taskfile.yml b/Taskfile.yml index e4826df..3d22459 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -63,11 +63,6 @@ tasks: cmds: - cargo check --release - test:nix: - desc: Build the default Nix package - cmds: - - nix build .# --no-link - deps:machete: desc: Check for unused dependencies cmds: @@ -103,7 +98,6 @@ tasks: - task lint - task test - task test:release-check - - task test:nix - task deps:machete - task deps:deny @@ -111,15 +105,3 @@ tasks: desc: Build the release binary cmds: - cargo build --release - - install: - desc: Install the local build with install.sh, in the same layout as a release (Linux x86_64 and macOS aarch64 only) - vars: - ROOT: '{{if eq OS "darwin"}}kagi-macos-aarch64{{else}}kagi-linux-x86_64{{end}}' - cmds: - - cargo build --release - - rm -rf target/dist - - mkdir -p target/dist/{{.ROOT}}/bin - - cp target/release/kagi-search target/release/kagi-maps target/release/kagi-summarize target/dist/{{.ROOT}}/bin/ - - tar -czf target/dist/{{.ROOT}}.tar.gz -C target/dist {{.ROOT}} - - KAGI_INSTALL_BASE_URL="file://$PWD/target/dist" KAGI_INSTALL_VERIFY=skip sh install.sh --force "v$(target/release/kagi-search --version | cut -d' ' -f2)" diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 128f5bc..0000000 --- a/flake.lock +++ /dev/null @@ -1,43 +0,0 @@ -{ - "nodes": { - "crane": { - "locked": { - "lastModified": 1789760033, - "narHash": "sha256-jtT4yxZpR8seYnlCMMWSSPlFN92zO6ICuZ8pJrmi86k=", - "owner": "ipetkov", - "repo": "crane", - "rev": "73b980519cefc727a5f6cc8e5c0947a2f9be6edd", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1790046670, - "narHash": "sha256-MYiI+CzL0tuWgRPjGsKCDHqYs2T3OzMlMQWOYWG0qso=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "6774f7bc253789b113a4f39285dc0fa100abeacc", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "crane": "crane", - "nixpkgs": "nixpkgs" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 47436cb..0000000 --- a/flake.nix +++ /dev/null @@ -1,165 +0,0 @@ -{ - description = "Unix-style CLI tools for Kagi search, maps, and URL summarization"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - - # crane splits the third-party dependency compile into its own cached - # derivation (cargoArtifacts), so a change to kagi's own source recompiles only - # kagi instead of all ~246 deps. crane is a pure lib (mkLib pkgs) with no - # nixpkgs input to follow. - crane.url = "github:ipetkov/crane"; - }; - - outputs = - { - self, - nixpkgs, - crane, - ... - }: - let - supportedSystems = [ - "aarch64-darwin" - "x86_64-linux" - ]; - - forAllSystems = nixpkgs.lib.genAttrs supportedSystems; - - pkgsFor = system: nixpkgs.legacyPackages.${system}; - - # crane's two stages share one argument set. cargoArtifacts compiles the - # dependency tree once (keyed on Cargo.toml + Cargo.lock, with kagi's own - # crate stubbed); the binary cache then serves it so a change to kagi's source - # recompiles only kagi. - craneFor = - pkgs: - let - craneLib = crane.mkLib pkgs; - commonArgs = { - pname = "kagi"; - version = "0.6.1"; - src = pkgs.lib.cleanSource ./.; - strictDeps = true; - nativeBuildInputs = [ - pkgs.cmake - pkgs.git - pkgs.pkg-config - pkgs.rustPlatform.bindgenHook - ]; - }; - in - { - inherit craneLib commonArgs; - cargoArtifacts = craneLib.buildDepsOnly commonArgs; - }; - - packageFor = - pkgs: - let - c = craneFor pkgs; - in - c.craneLib.buildPackage ( - c.commonArgs - // { - inherit (c) cargoArtifacts; - # The suite runs as its own check (checksFor) and in CI via `task test`; - # the installable package only builds the binaries. - doCheck = false; - } - ); - - # `cargo test`, reusing the cached dependency artifacts so it does not - # recompile the dependency tree. Keeps `nix flake check` running the suite the - # way the old `doCheck = true` package did. - checksFor = - pkgs: - let - c = craneFor pkgs; - in - c.craneLib.cargoTest (c.commonArgs // { inherit (c) cargoArtifacts; }); - - devShellFor = - pkgs: - pkgs.mkShell { - # crane builds with the nixpkgs rustc and cargo, so `nix build` and - # `task check` compile with the same toolchain. - packages = [ - pkgs.cargo - pkgs.cargo-deny - pkgs.cargo-machete - pkgs.clippy - pkgs.cmake - pkgs.git - pkgs.go-task - pkgs.nodejs - pkgs.pkg-config - pkgs.rustc - pkgs.rustfmt - pkgs.zig - ]; - - inputsFrom = [ (packageFor pkgs) ]; - }; - in - { - packages = forAllSystems ( - system: - let - pkgs = pkgsFor system; - kagi = packageFor pkgs; - in - { - default = kagi; - kagi = kagi; - } - ); - - checks = forAllSystems ( - system: - let - pkgs = pkgsFor system; - in - { - default = checksFor pkgs; - } - ); - - devShells = forAllSystems ( - system: - let - pkgs = pkgsFor system; - in - { - default = devShellFor pkgs; - } - ); - - homeManagerModules.default = - { - config, - lib, - pkgs, - ... - }: - let - cfg = config.programs.kagi; - package = self.packages.${pkgs.stdenv.hostPlatform.system}.default; - in - { - options.programs.kagi = { - enable = lib.mkEnableOption "Kagi search, maps, and summarization CLIs"; - - package = lib.mkOption { - type = lib.types.package; - default = package; - description = "Kagi package to install."; - }; - }; - - config = lib.mkIf cfg.enable { - home.packages = [ cfg.package ]; - }; - }; - }; -} diff --git a/install.sh b/install.sh deleted file mode 100644 index 08cd32a..0000000 --- a/install.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env sh -# Install or update kagi-search, kagi-maps, and kagi-summarize. -# -# usage: install.sh [--check] [--force] [] -# -# Without a tag it installs the latest release. When the installed -# kagi-search --version already matches, it says so and changes nothing; -# --force installs anyway. --check installs nothing: it exits 0 when the -# installed version matches the target and 100 when kagi is not installed or -# has a different version. -set -eu - -usage="usage: install.sh [--check] [--force] []" -repo="${KAGI_INSTALL_REPO:-rubas/kagi}" -version="${KAGI_INSTALL_VERSION:-}" -verify="${KAGI_INSTALL_VERIFY:-auto}" -check=false -force=false - -for arg; do - case "$arg" in - --check) check=true ;; - --force) force=true ;; - -*) - echo "$usage" >&2 - exit 1 - ;; - *) version="$arg" ;; - esac -done - -# KAGI_INSTALL_VERIFY: auto (default) verifies the GitHub build provenance -# attestations when gh is available and warns otherwise; require fails without -# verification; skip disables it. -case "$verify" in -auto | require | skip) ;; -*) - echo "invalid KAGI_INSTALL_VERIFY value: $verify (expected auto, require, or skip)" >&2 - exit 1 - ;; -esac - -os="$(uname -s)" -arch="$(uname -m)" - -case "$os/$arch" in -Linux/x86_64) root="kagi-linux-x86_64" ;; -Darwin/arm64) root="kagi-macos-aarch64" ;; -*) - echo "unsupported platform: $os/$arch" >&2 - exit 1 - ;; -esac - -if [ -z "$version" ]; then - if [ -n "${KAGI_INSTALL_BASE_URL:-}" ]; then - echo "KAGI_INSTALL_BASE_URL needs a version tag" >&2 - exit 1 - fi - # The web redirect of /releases/latest names the tag; unlike the API it needs - # no token and has no rate limit. - url="$(curl -fsSIL -o /dev/null -w '%{url_effective}' "https://github.com/${repo}/releases/latest")" || exit 1 - case "$url" in - */releases/tag/*) version="${url##*/}" ;; - *) - echo "no release found at https://github.com/${repo}/releases/latest" >&2 - exit 1 - ;; - esac -fi - -bin_dir="${HOME}/.local/bin" -target="${version#v}" -installed="$("${bin_dir}/kagi-search" --version 2>/dev/null)" || installed="" -installed="${installed#kagi-search }" - -if [ "$installed" = "$target" ]; then - echo "kagi ${target} is current" - if $check || ! $force; then exit 0; fi -else - echo "kagi: ${installed:-not installed} -> ${target}" - if $check; then exit 100; fi -fi - -# KAGI_INSTALL_BASE_URL points at local archives (file:// URL): the release -# smoke test, task install, and a host that verified the archives itself. -base_url="${KAGI_INSTALL_BASE_URL:-https://github.com/${repo}/releases/download/${version}}" - -tmp="$(mktemp -d)" -trap 'rm -rf "$tmp"' EXIT -trap 'exit 1' INT TERM - -curl -fsSL "${base_url}/${root}.tar.gz" -o "$tmp/${root}.tar.gz" - -if [ "$verify" != "skip" ]; then - if command -v gh >/dev/null 2>&1; then - gh attestation verify "$tmp/${root}.tar.gz" --repo "$repo" - elif [ "$verify" = "require" ]; then - echo "KAGI_INSTALL_VERIFY=require but the gh CLI is not available to verify attestations" >&2 - exit 1 - else - echo "warning: gh CLI not found; skipping release attestation verification" >&2 - echo "warning: install the GitHub CLI or set KAGI_INSTALL_VERIFY=require to fail instead" >&2 - fi -fi - -tar -xzf "$tmp/${root}.tar.gz" -C "$tmp" - -# Fail before touching any installed files if the archive layout drifted. -for file in bin/kagi-search bin/kagi-maps bin/kagi-summarize; do - if [ ! -f "$tmp/$root/$file" ]; then - echo "unexpected archive layout: missing $root/$file" >&2 - exit 1 - fi -done - -# kagi-search goes last: its --version marks the install as current, so an -# install that fails before it runs again in full. -install -d "$bin_dir" -for bin in kagi-maps kagi-summarize kagi-search; do - install -m 755 "$tmp/$root/bin/$bin" "$bin_dir/$bin" -done -echo "installed kagi ${target}: kagi-search, kagi-maps, and kagi-summarize in ${bin_dir}"