diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 575aed6..34dfaf8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Verify tag is on main run: git merge-base --is-ancestor ${{ github.sha }} origin/main - test: + unit-test: needs: guard runs-on: macos-15 steps: @@ -31,6 +31,16 @@ jobs: - name: Unit tests run: make test + integration-test: + needs: guard + runs-on: macos-15 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version: '1.24' + - name: Integration tests env: GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} @@ -38,7 +48,7 @@ jobs: run: make test-integration release: - needs: test + needs: [unit-test, integration-test] runs-on: macos-15 steps: - uses: actions/checkout@v5 @@ -67,34 +77,4 @@ jobs: chmod +x "$universal" packaging/mkdmg.sh "$universal" "$VERSION" "Zee-${VERSION}.dmg" shasum -a 256 "Zee-${VERSION}.dmg" >> dist/checksums.txt - gh release upload "$VERSION" "Zee-${VERSION}.dmg" --clobber - - - name: Update Homebrew cask - env: - VERSION: ${{ github.ref_name }} - GITHUB_TOKEN: ${{ secrets.ZEE_RELEASE_TOKEN }} - run: | - CLEAN_VERSION="${VERSION#v}" - SHA256=$(shasum -a 256 "Zee-${VERSION}.dmg" | awk '{print $1}') - cat > zee.rb <.dmg` -2. Open the DMG and drag `Zee.app` to `/Applications` -3. On first launch, grant **Microphone** and **Accessibility** permissions to the terminal or Zee.app via System Settings > Privacy & Security -4. Set `GROQ_API_KEY` in your shell profile or launch via: `GROQ_API_KEY=xxx open /Applications/Zee.app` +End users install via: + +```bash +curl -fsSL https://raw.githubusercontent.com/sumerc/zee/main/install.sh | bash +``` + +`install.sh` downloads the latest DMG from GitHub Releases, verifies its SHA256 against `checksums.txt`, copies `Zee.app` to `/Applications`, and runs `xattr -cr` to clear quarantine. Permissions (Microphone, Accessibility) are still granted lazily by macOS TCC on first use — installers cannot pre-grant them. + +Local dev DMG: `make app` produces `Zee-.dmg`; drag to `/Applications`. ## Testing @@ -32,7 +37,6 @@ make benchmark WAV=file.wav RUNS=5 ## Flags -- `-stream` - enable streaming transcription (Deepgram only) - `-debug` - enable diagnostic logging (default: false) - `-debug-transcribe` - enable transcription text logging (requires `-debug`) - `-format ` - audio format (default: mp3@16) @@ -106,12 +110,11 @@ git tag v0.3.0 && git push origin v0.3.0 # triggers CI release CI (`.github/workflows/release.yml`) does: 1. Verify tag is on `main` 2. GoReleaser builds arm64 + amd64 binaries, universal binary, tar.gz archives, checksums, GitHub release -3. GoReleaser pushes Homebrew formula to `sumerc/homebrew-tap` -4. Post-step creates DMG from universal binary and uploads to release +3. Post-step creates DMG from universal binary, appends its SHA256 to `checksums.txt`, uploads to release -Requires `ZEE_RELEASE_TOKEN` repo secret (fine-grained PAT with Contents read/write on `zee` + `homebrew-tap`). +Requires `ZEE_RELEASE_TOKEN` repo secret (fine-grained PAT with Contents read/write on `zee`). -Users install via: `brew install sumerc/tap/zee` +Users install via the one-liner in README (`install.sh` fetches the DMG and verifies the checksum). ## Packaging diff --git a/Makefile b/Makefile index 596062d..598f2df 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-linux-amd64 build-linux-arm64 test test-integration benchmark integration-test clean release icns app +.PHONY: build build-linux-amd64 build-linux-arm64 test test-integration benchmark integration-test clean bump-version release icns app VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") @@ -40,10 +40,33 @@ app: build icns clean: rm -f zee Zee-*.dmg +bump-version: + @branch=$$(git rev-parse --abbrev-ref HEAD); \ + if [ "$$branch" != "main" ]; then echo "ERROR: must be on main branch" && exit 1; fi; \ + ver="$(VER)"; \ + if [ -z "$$ver" ]; then echo "usage: make bump-version VER=0.3.7" && exit 1; fi; \ + latest=$$(git tag --sort=-v:refname | head -1); \ + claude -p "Look at the git log from tag $$latest to HEAD. Write a CHANGELOG.md entry for Zee version v$$ver in this exact format: ## v$$ver, blank line, then markdown groups using ### Added, ### Changed, ### Fixed, ### Removed only when relevant, with concise '- ' bullets. Skip merge commits and CI-only changes. Output ONLY the changelog entry, no code fences." > /tmp/zee-changelog-entry; \ + echo "" >> /tmp/zee-changelog-entry; \ + sed -i '' '/^## Unreleased/r /tmp/zee-changelog-entry' CHANGELOG.md; \ + rm -f /tmp/zee-changelog-entry; \ + echo "CHANGELOG.md updated — review and edit as needed" + release: - @latest=$$(gh release view --json tagName -q .tagName 2>/dev/null || echo "none"); \ - echo "latest release: $$latest"; \ - read -p "new version (e.g. 0.2.0): " ver; \ - test -n "$$ver" || (echo "aborted" && exit 1); \ - echo "tagging v$$ver and pushing..."; \ - git tag "v$$ver" && git push origin "v$$ver" + @branch=$$(git rev-parse --abbrev-ref HEAD); \ + if [ "$$branch" != "main" ]; then echo "ERROR: must be on main branch" && exit 1; fi; \ + ver="$(VER)"; \ + if [ -z "$$ver" ]; then echo "usage: make release VER=0.3.7" && exit 1; fi; \ + grep -q "^## v$$ver$$" CHANGELOG.md || (echo "ERROR: v$$ver missing from CHANGELOG.md — run make bump-version first" && exit 1); \ + git diff --quiet || (echo "ERROR: working tree has uncommitted changes" && exit 1); \ + git diff --cached --quiet || (echo "ERROR: index has staged changes" && exit 1); \ + notes=$$(awk "/^## v$$ver$$/{found=1; next} /^## /{if(found) exit} found{print}" CHANGELOG.md | sed '/^$$/d'); \ + echo ""; \ + echo "v$$ver Release Notes:"; \ + echo ""; \ + echo "$$notes"; \ + echo ""; \ + read -p "create and push tag v$$ver? [y/N] " confirm; \ + case "$$confirm" in y|Y) ;; *) echo "aborted" && exit 1;; esac; \ + git tag -a "v$$ver" -m "v$$ver"; \ + git push origin "v$$ver" diff --git a/README.md b/README.md index 3ead5c8..ee78eb4 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ - **System tray app** — lives in the menu bar. Switch microphones, transcription providers, and languages from the tray menu. Dynamic icons show recording and warning states. - **Two recording modes** — push-to-talk (hold hotkey) or tap-to-toggle (tap to start/stop). -- **Real-time streaming** — with `-stream`, words appear as you speak and auto-paste into the focused window incrementally. Powered by Deepgram's WebSocket API. +- **Real-time streaming** — when a streaming-capable model is selected (e.g. Deepgram Nova-3), words appear as you speak and auto-paste into the focused window incrementally. - **Fast batch mode** — HTTP keep-alive, TLS connection reuse, pre-warmed connections, streaming encoder runs during recording (not after). Typical key-release to clipboard: under 500ms. - **Auto-paste** — transcribed text goes straight to clipboard and pastes into the active window. In streaming mode, each new phrase pastes as it arrives. - **Silence detection** — VAD-based voice activity detection warns when no speech is heard. In streaming mode, auto-closes recording after 30 seconds of silence. @@ -31,35 +31,26 @@ ## Install -### Homebrew (recommended) +### One-liner (recommended) ```bash -brew install --cask sumerc/tap/zee +curl -fsSL https://raw.githubusercontent.com/sumerc/zee/main/install.sh | bash ``` -Installs `Zee.app` to `/Applications`. Launch from Spotlight or the Applications folder. +Downloads the latest DMG, verifies its SHA256 against `checksums.txt`, copies `Zee.app` to `/Applications`, and clears the quarantine attribute. Pin a version with `VERSION=vX.Y.Z bash`. -On first launch, macOS may warn that the app can't be verified. Run once to clear it: - -```bash -xattr -cr /Applications/Zee.app -``` - -### macOS (DMG) +### Manual DMG 1. Download `Zee-.dmg` from the [latest release](https://github.com/sumerc/zee/releases/latest) 2. Open the DMG and drag **Zee.app** to **Applications** +3. Clear quarantine: `xattr -cr /Applications/Zee.app` ### CLI binary -For terminal usage, install the formula or download directly: - -```bash -brew install sumerc/tap/zee # installs to /opt/homebrew/bin/zee -``` +For terminal usage: ```bash -# or download manually — Apple Silicon +# Apple Silicon curl -L https://github.com/sumerc/zee/releases/latest/download/zee_darwin_arm64.tar.gz | tar xz # Intel @@ -67,9 +58,9 @@ curl -L https://github.com/sumerc/zee/releases/latest/download/zee_darwin_amd64. ``` ```bash -GROQ_API_KEY=xxx zee # Groq Whisper -OPENAI_API_KEY=xxx zee -stream # Deepgram streaming -zee -debug # with diagnostic logging +GROQ_API_KEY=xxx ./zee # Groq Whisper +DEEPGRAM_API_KEY=xxx ./zee # Deepgram (streaming auto-enabled when a streaming model is selected from the tray) +./zee -debug # with diagnostic logging ``` > **Note:** When running from a terminal, macOS permissions (Microphone, Accessibility) are granted to the **terminal app** (e.g. Ghostty, iTerm2, Terminal), not to zee itself. @@ -92,7 +83,6 @@ export OPENAI_API_KEY=your_key # batch mode (OpenAI Whisper) export DEEPGRAM_API_KEY=your_key # streaming mode (Deepgram) export MISTRAL_API_KEY=your_key # batch mode (Mistral Voxtral) zee # starts in menu bar, hold Ctrl+Shift+Space to record -zee -stream # words appear as you speak ``` > **Note:** `export` only works in the current terminal session. To make API keys available to `Zee.app` when launched from Spotlight or Applications, use `launchctl`: @@ -128,7 +118,6 @@ make benchmark WAV=file.wav RUNS=5 # multiple runs for timing | Flag | Default | Description | |------|---------|-------------| -| `-stream` | false | Real-time streaming transcription (Deepgram) | | `-format` | mp3@16 | Audio format: `mp3@16`, `mp3@64`, or `flac` | | `-autopaste` | true | Auto-paste into focused window | | `-setup` | false | Select microphone device | diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..21d0fe9 --- /dev/null +++ b/install.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Zee installer for macOS. +# Usage: curl -fsSL https://raw.githubusercontent.com/sumerc/zee/main/install.sh | bash +# VERSION=vX.Y.Z bash install.sh +set -euo pipefail + +REPO="sumerc/zee" +APP_DIR="/Applications" +TMP="$(mktemp -d)" +MOUNT="" + +err() { echo "error: $*" >&2; exit 1; } +log() { echo "==> $*"; } +cleanup() { + [[ -n "$MOUNT" ]] && hdiutil detach "$MOUNT" -quiet >/dev/null 2>&1 || true + rm -rf "$TMP" +} +run_or_sudo() { + "$@" 2>/dev/null || { log "Need sudo: $*"; sudo "$@"; } +} +trap cleanup EXIT + +[[ "$(uname -s)" == "Darwin" ]] || err "Zee currently supports macOS only." + +VERSION="${VERSION:-}" +if [[ -z "$VERSION" ]]; then + log "Resolving latest release..." + VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ + | awk -F'"' '/"tag_name"/ {print $4; exit}')" + [[ -n "$VERSION" ]] || err "could not resolve latest version (GitHub API rate limit?). Set VERSION=vX.Y.Z and retry." +fi +log "Installing Zee ${VERSION}" + +DMG="Zee-${VERSION}.dmg" +BASE="https://github.com/${REPO}/releases/download/${VERSION}" + +log "Downloading ${DMG}..." +curl -fL --progress-bar "${BASE}/${DMG}" -o "${TMP}/${DMG}" \ + || err "download failed: ${BASE}/${DMG}" + +log "Verifying checksum..." +curl -fsSL "${BASE}/checksums.txt" -o "${TMP}/checksums.txt" \ + || err "download failed: ${BASE}/checksums.txt" +expected="$(awk -v f="${DMG}" '$2==f {print $1}' "${TMP}/checksums.txt")" +[[ -n "$expected" ]] || err "${DMG} not found in checksums.txt" +actual="$(shasum -a 256 "${TMP}/${DMG}" | awk '{print $1}')" +[[ "$expected" == "$actual" ]] || err "checksum mismatch (expected $expected, got $actual)" +log "Checksum OK" + +log "Mounting DMG..." +MOUNT="$(hdiutil attach -nobrowse -readonly -mountrandom /tmp "${TMP}/${DMG}" \ + | grep -oE '/(private/tmp|Volumes)/[^[:space:]]+' \ + | tail -1)" +[[ -n "$MOUNT" && -d "$MOUNT/Zee.app" ]] || err "Zee.app not found in DMG" + +if [[ -d "${APP_DIR}/Zee.app" ]]; then + log "Removing existing ${APP_DIR}/Zee.app" + run_or_sudo rm -rf "${APP_DIR}/Zee.app" +fi + +log "Copying Zee.app to ${APP_DIR}..." +run_or_sudo cp -R "$MOUNT/Zee.app" "${APP_DIR}/" + +log "Clearing quarantine attribute..." +run_or_sudo xattr -cr "${APP_DIR}/Zee.app" + +cat <