add installation process and github workflow

This commit is contained in:
patriceckhart 2026-04-18 10:50:02 +02:00
parent 6aea65ea02
commit 4712f5e0e3
8 changed files with 545 additions and 4 deletions

48
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,48 @@
name: ci
on:
push:
branches: [main]
pull_request:
branches: [main]
# Cancel in-progress runs when a new commit lands on the same branch /
# pr, so we don't waste minutes on stale SHAs.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: set up go
uses: actions/setup-go@v5
with:
go-version: "1.23"
cache: true
- name: go vet
run: go vet ./...
- name: gofmt check
# Windows' bash is mingw; gofmt outputs paths with slashes so the
# diff check works there too. Skip on windows only if needed.
if: runner.os != 'Windows'
run: |
diff=$(gofmt -l .)
if [ -n "$diff" ]; then
echo "gofmt issues:"
echo "$diff"
exit 1
fi
- name: go test
run: go test -race ./...

43
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: release
on:
push:
tags:
- "v*"
permissions:
# Needed for GoReleaser to create the GitHub Release and upload assets.
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
with:
# GoReleaser needs the full history to build a useful
# changelog from commit messages.
fetch-depth: 0
- name: set up go
uses: actions/setup-go@v5
with:
go-version: "1.23"
cache: true
- name: run goreleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release --clean
env:
# Default token for the release itself (uploads artifacts,
# creates the release page).
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Optional: PAT with `repo` scope for patriceckhart/homebrew-tap
# so we can push a Formula/zot.rb update on every release.
# Create the secret in repo settings → actions → secrets.
# If unset, goreleaser skips the brew step (skip_upload: auto).
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}

122
.goreleaser.yaml Normal file
View file

@ -0,0 +1,122 @@
# GoReleaser config — builds cross-platform binaries and GitHub Release
# artifacts for zot.
#
# Triggered by pushing a tag like `v0.1.0`. The release workflow runs
# `goreleaser release --clean`, which compiles every target, packs
# them into `.tar.gz` (Unix) / `.zip` (Windows) archives, generates a
# checksum file, and uploads everything to a GitHub Release.
version: 2
project_name: zot
before:
hooks:
- go mod tidy
builds:
- id: zot
main: ./cmd/zot
binary: zot
env:
- CGO_ENABLED=0
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
# Windows/arm64 is shipped by Go but rarely used — skip to keep the
# release artifact list tidy.
ignore:
- goos: windows
goarch: arm64
flags:
- -trimpath
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
archives:
- id: zot
# Human-friendly name: zot_0.1.0_darwin_arm64.tar.gz
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- LICENSE
- README.md
format_overrides:
- goos: windows
format: zip
checksum:
name_template: "checksums.txt"
algorithm: sha256
snapshot:
# Used by `goreleaser release --snapshot`; lets contributors build
# a local release without pushing a tag.
version_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
use: github
filters:
exclude:
- "^docs:"
- "^chore:"
- "^ci:"
- "^test:"
groups:
- title: features
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: fixes
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: other
order: 999
release:
github:
owner: patriceckhart
name: zot
draft: false
prerelease: auto
# Auto-generated release notes include the changelog groups above plus
# an install snippet so the release page itself is a landing page.
header: |
## zot {{ .Tag }}
one-liner install:
```bash
curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash
```
homebrew:
```bash
brew install patriceckhart/tap/zot
```
or download a binary below, `chmod +x`, and drop it on your `$PATH`.
# Optional: publish a Homebrew formula to a tap repo on every release.
# Requires the tap repo to exist at github.com/patriceckhart/homebrew-tap
# and a PAT with `repo` scope exported as `HOMEBREW_TAP_TOKEN` in the
# release workflow. Safe to leave enabled even before the tap exists;
# goreleaser skips it when the token isn't set.
brews:
- repository:
owner: patriceckhart
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
name: zot
homepage: https://github.com/patriceckhart/zot
description: "lightweight coding agent harness — anthropic + openai, four tools, tui"
license: MIT
install: |
bin.install "zot"
test: |
system "#{bin}/zot", "--help"
# Only publish the formula when the token is actually set, so
# ordinary tag pushes from forks don't explode.
skip_upload: auto

View file

@ -1,4 +1,6 @@
VERSION ?= dev
# Local / untagged builds ship as 0.0.0. Release builds are driven by
# goreleaser which overrides VERSION from the git tag.
VERSION ?= 0.0.0
LDFLAGS := -s -w -X main.version=$(VERSION)
.PHONY: build install test lint fmt clean release

View file

@ -12,11 +12,39 @@ yet another coding agent harness, lightweight and written (vibe-slopped) in go.
## install
### one-liner (macos + linux)
```bash
curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash
```
detects your os/arch, downloads the latest release from github, verifies the sha256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin` — whichever is writable first. pass a version or prefix to pin:
```bash
curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash -s -- v0.0.1 ~/bin
```
### one-liner (windows, powershell)
```powershell
iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex
```
drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. open a fresh terminal afterwards.
### homebrew (macos + linux)
```bash
brew install patriceckhart/tap/zot
```
### go install
```bash
go install github.com/patriceckhart/zot/cmd/zot@latest
```
or from source:
### from source
```bash
git clone https://github.com/patriceckhart/zot
@ -25,6 +53,10 @@ make build # produces ./bin/zot
make install # into $GOPATH/bin
```
### prebuilt binaries
every release on the [releases page](https://github.com/patriceckhart/zot/releases) ships archives for linux, macos, and windows on amd64 + arm64 (except windows/arm64), plus a `checksums.txt` file. download, verify, `chmod +x`, and drop on your `$PATH`.
## authenticate
the easiest way is to just run `zot` and type `/login`. the tui opens even without credentials and walks you through a browser-based login flow.

View file

@ -8,10 +8,33 @@ import (
"github.com/patriceckhart/zot/internal/agent"
)
var version = "dev"
// Injected at build time via -ldflags "-X main.version=... -X main.commit=... -X main.date=...".
// See .goreleaser.yaml for the release build and the Makefile for
// local builds. Defaults make `zot --version` print something sensible
// when built without ldflags.
var (
// 0.0.0 is the pre-release placeholder for local / untagged
// builds. The first published GitHub release will be tagged
// v0.0.1; everything before that ships as 0.0.0 from source.
version = "0.0.0"
commit = ""
date = ""
)
func main() {
if err := agent.Run(os.Args[1:], version); err != nil {
v := version
if commit != "" {
short := commit
if len(short) > 7 {
short = short[:7]
}
v = v + " (" + short
if date != "" {
v = v + ", " + date
}
v = v + ")"
}
if err := agent.Run(os.Args[1:], v); err != nil {
fmt.Fprintln(os.Stderr, "zot:", err)
os.Exit(1)
}

115
install.ps1 Normal file
View file

@ -0,0 +1,115 @@
# zot installer for Windows (PowerShell).
#
# Usage (in PowerShell):
# iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex
#
# Or with arguments:
# $env:ZOT_VERSION = "v0.0.1"
# $env:ZOT_PREFIX = "$HOME\bin"
# iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex
#
# Detects architecture, downloads the matching .zip from the GitHub
# release, verifies the sha256 against checksums.txt, extracts zot.exe,
# and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH
# via the User environment if missing).
[CmdletBinding()]
param(
[string]$Version = $env:ZOT_VERSION,
[string]$Prefix = $env:ZOT_PREFIX
)
$ErrorActionPreference = "Stop"
$owner = "patriceckhart"
$repo = "zot"
$binary = "zot"
if (-not $Version) { $Version = "latest" }
if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" }
function Msg($m) { Write-Host "==> $m" -ForegroundColor Cyan }
function Warn($m) { Write-Warning $m }
function Die($m) { Write-Error $m; exit 1 }
# ---- detect architecture ----
switch -wildcard ($env:PROCESSOR_ARCHITECTURE) {
"AMD64" { $arch = "amd64" }
"ARM64" { $arch = "arm64" }
default { Die "unsupported arch: $($env:PROCESSOR_ARCHITECTURE)" }
}
# ARM64 Windows isn't shipped (see .goreleaser.yaml ignore rule) — fall
# back to amd64 which runs fine under ARM64 emulation.
if ($arch -eq "arm64") {
Warn "windows/arm64 is not published; falling back to amd64"
$arch = "amd64"
}
# ---- resolve version ----
if ($Version -eq "latest") {
$resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 `
"https://github.com/$owner/$repo/releases/latest"
if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') {
$Version = $Matches[1]
} else {
Die "could not resolve latest version"
}
}
if (-not $Version.StartsWith("v")) { $Version = "v$Version" }
$verNum = $Version.TrimStart("v")
# ---- download + verify + extract ----
$archive = "${binary}_${verNum}_windows_${arch}.zip"
$baseUrl = "https://github.com/$owner/$repo/releases/download/$Version"
$archiveUrl = "$baseUrl/$archive"
$checksumUrl = "$baseUrl/checksums.txt"
$tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("zot-install-" + [System.Guid]::NewGuid().ToString("N").Substring(0,8)))
try {
Msg "downloading $archive"
Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)
Msg "verifying checksum"
$checksums = Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl | Select-Object -ExpandProperty Content
$expected = ($checksums -split "`n" | Where-Object { $_ -match [regex]::Escape($archive) + "$" } | Select-Object -First 1)
if (-not $expected) { Die "no checksum for $archive in checksums.txt" }
$expectedHash = ($expected -split "\s+")[0]
$actualHash = (Get-FileHash -Path (Join-Path $tmp $archive) -Algorithm SHA256).Hash.ToLower()
if ($expectedHash.ToLower() -ne $actualHash) {
Die "checksum mismatch: expected $expectedHash, got $actualHash"
}
Msg "extracting"
Expand-Archive -Path (Join-Path $tmp $archive) -DestinationPath $tmp -Force
$exe = Join-Path $tmp "$binary.exe"
if (-not (Test-Path $exe)) { Die "archive did not contain $binary.exe" }
Msg "installing to $Prefix\$binary.exe"
New-Item -ItemType Directory -Path $Prefix -Force | Out-Null
Copy-Item $exe (Join-Path $Prefix "$binary.exe") -Force
# ---- PATH hint ----
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
$parts = $userPath -split ";" | Where-Object { $_ }
if (-not ($parts -contains $Prefix)) {
Warn "$Prefix is not on your user PATH"
Warn "adding it for future sessions..."
[Environment]::SetEnvironmentVariable("Path", ($userPath.TrimEnd(";") + ";" + $Prefix), "User")
Warn "open a new terminal to pick up the change, or run:"
Warn " `$env:Path = `"$Prefix;`$env:Path`""
}
Msg "installed. run: zot --help"
}
finally {
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}

156
install.sh Executable file
View file

@ -0,0 +1,156 @@
#!/usr/bin/env bash
#
# zot installer.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash
# curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash -s -- v0.0.1 ~/bin
#
# Positional arguments:
# $1 version — release tag (e.g. v0.0.1). Defaults to "latest".
# $2 prefix — install directory. Defaults to the first writable
# directory in: /usr/local/bin, $HOME/.local/bin,
# $HOME/bin. Created if missing. Add it to your PATH
# if it isn't already.
#
# Environment overrides:
# ZOT_VERSION same as $1
# ZOT_PREFIX same as $2
#
# The script detects your OS and architecture, downloads the matching
# archive from the GitHub release, verifies the sha256 against the
# release's checksums.txt, extracts the binary, and moves it into the
# prefix directory. No sudo unless you explicitly pick a prefix that
# needs it.
set -euo pipefail
OWNER="patriceckhart"
REPO="zot"
BINARY="zot"
VERSION="${1:-${ZOT_VERSION:-latest}}"
PREFIX="${2:-${ZOT_PREFIX:-}}"
msg() { printf "\033[1m==>\033[0m %s\n" "$*"; }
warn() { printf "\033[33mwarn:\033[0m %s\n" "$*" >&2; }
die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; }
command -v curl >/dev/null 2>&1 || die "curl is required"
command -v tar >/dev/null 2>&1 || die "tar is required"
# ---- detect OS + arch ----
uname_s=$(uname -s)
uname_m=$(uname -m)
case "$uname_s" in
Linux) OS=linux ;;
Darwin) OS=darwin ;;
MINGW*|MSYS*|CYGWIN*)
die "windows detected — use install.ps1 from powershell instead"
;;
*) die "unsupported os: $uname_s" ;;
esac
case "$uname_m" in
x86_64|amd64) ARCH=amd64 ;;
arm64|aarch64) ARCH=arm64 ;;
*) die "unsupported arch: $uname_m" ;;
esac
# ---- resolve version ----
if [ "$VERSION" = "latest" ]; then
# GitHub's /releases/latest endpoint redirects to /releases/tag/<tag>.
# We follow the redirect and grab the tag from the final URL without
# needing jq.
VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
"https://github.com/${OWNER}/${REPO}/releases/latest" \
| sed -E 's|.*/tag/([^/]+).*|\1|')
[ -n "$VERSION" ] || die "could not resolve latest version"
fi
case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac
VER_NUM="${VERSION#v}"
# ---- pick an install prefix ----
pick_prefix() {
local candidates=()
[ -n "$PREFIX" ] && { echo "$PREFIX"; return; }
candidates+=("/usr/local/bin")
[ -n "${HOME:-}" ] && candidates+=("$HOME/.local/bin" "$HOME/bin")
for d in "${candidates[@]}"; do
if [ -d "$d" ] && [ -w "$d" ]; then
echo "$d"
return
fi
done
# Nothing writable yet — create ~/.local/bin and use that.
if [ -n "${HOME:-}" ]; then
mkdir -p "$HOME/.local/bin"
echo "$HOME/.local/bin"
return
fi
die "no writable install prefix found; pass one as the second argument"
}
PREFIX=$(pick_prefix)
mkdir -p "$PREFIX"
# ---- download + verify + extract ----
ARCHIVE="${BINARY}_${VER_NUM}_${OS}_${ARCH}.tar.gz"
BASE_URL="https://github.com/${OWNER}/${REPO}/releases/download/${VERSION}"
ARCHIVE_URL="${BASE_URL}/${ARCHIVE}"
CHECKSUMS_URL="${BASE_URL}/checksums.txt"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
msg "downloading ${ARCHIVE}"
curl -fsSL -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \
|| die "download failed: $ARCHIVE_URL"
msg "verifying checksum"
curl -fsSL -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \
|| die "download failed: $CHECKSUMS_URL"
expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true)
[ -n "$expected" ] || die "no checksum for $ARCHIVE in checksums.txt"
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$TMP/$ARCHIVE" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
actual=$(shasum -a 256 "$TMP/$ARCHIVE" | awk '{print $1}')
else
die "no sha256 tool found (sha256sum or shasum)"
fi
[ "$expected" = "$actual" ] \
|| die "checksum mismatch: expected $expected, got $actual"
msg "extracting"
tar -xzf "$TMP/$ARCHIVE" -C "$TMP"
[ -f "$TMP/$BINARY" ] || die "archive did not contain a '$BINARY' binary"
msg "installing to $PREFIX/$BINARY"
install -m 0755 "$TMP/$BINARY" "$PREFIX/$BINARY" 2>/dev/null \
|| { cp "$TMP/$BINARY" "$PREFIX/$BINARY" && chmod 0755 "$PREFIX/$BINARY"; }
# ---- PATH hint ----
case ":$PATH:" in
*":$PREFIX:"*) ;;
*)
warn "$PREFIX is not on your PATH"
warn "add this to your shell rc file:"
warn " export PATH=\"$PREFIX:\$PATH\""
;;
esac
msg "installed $("$PREFIX/$BINARY" --version 2>/dev/null || echo zot)"
msg "run: zot (interactive tui)"
msg "run: zot --help (all flags and subcommands)"