detached 4712f5e

add installation process and github workflow

commit
4712f5e0e3ce8df72266f0d54e8a4b52f9eb36a3
author
patriceckhart · · 2026-04-18 08:50 UTC
parent
6aea65e
signature
unsigned
8 files · +545 -4
A .github/workflows/ci.yml +48 -0
@@ -0,0 +1,48 @@
1+name: ci
2+
3+on:
4+ push:
5+ branches: [main]
6+ pull_request:
7+ branches: [main]
8+
9+# Cancel in-progress runs when a new commit lands on the same branch /
10+# pr, so we don't waste minutes on stale SHAs.
11+concurrency:
12+ group: ci-${{ github.ref }}
13+ cancel-in-progress: true
14+
15+jobs:
16+ test:
17+ name: test (${{ matrix.os }})
18+ runs-on: ${{ matrix.os }}
19+ strategy:
20+ fail-fast: false
21+ matrix:
22+ os: [ubuntu-latest, macos-latest, windows-latest]
23+ steps:
24+ - uses: actions/checkout@v4
25+
26+ - name: set up go
27+ uses: actions/setup-go@v5
28+ with:
29+ go-version: "1.23"
30+ cache: true
31+
32+ - name: go vet
33+ run: go vet ./...
34+
35+ - name: gofmt check
36+ # Windows' bash is mingw; gofmt outputs paths with slashes so the
37+ # diff check works there too. Skip on windows only if needed.
38+ if: runner.os != 'Windows'
39+ run: |
40+ diff=$(gofmt -l .)
41+ if [ -n "$diff" ]; then
42+ echo "gofmt issues:"
43+ echo "$diff"
44+ exit 1
45+ fi
46+
47+ - name: go test
48+ run: go test -race ./...
@@ -0,0 +1,48 @@
1+name: ci
2+
3+on:
4+ push:
5+ branches: [main]
6+ pull_request:
7+ branches: [main]
8+
9+# Cancel in-progress runs when a new commit lands on the same branch /
10+# pr, so we don't waste minutes on stale SHAs.
11+concurrency:
12+ group: ci-${{ github.ref }}
13+ cancel-in-progress: true
14+
15+jobs:
16+ test:
17+ name: test (${{ matrix.os }})
18+ runs-on: ${{ matrix.os }}
19+ strategy:
20+ fail-fast: false
21+ matrix:
22+ os: [ubuntu-latest, macos-latest, windows-latest]
23+ steps:
24+ - uses: actions/checkout@v4
25+
26+ - name: set up go
27+ uses: actions/setup-go@v5
28+ with:
29+ go-version: "1.23"
30+ cache: true
31+
32+ - name: go vet
33+ run: go vet ./...
34+
35+ - name: gofmt check
36+ # Windows' bash is mingw; gofmt outputs paths with slashes so the
37+ # diff check works there too. Skip on windows only if needed.
38+ if: runner.os != 'Windows'
39+ run: |
40+ diff=$(gofmt -l .)
41+ if [ -n "$diff" ]; then
42+ echo "gofmt issues:"
43+ echo "$diff"
44+ exit 1
45+ fi
46+
47+ - name: go test
48+ run: go test -race ./...
A .github/workflows/release.yml +43 -0
@@ -0,0 +1,43 @@
1+name: release
2+
3+on:
4+ push:
5+ tags:
6+ - "v*"
7+
8+permissions:
9+ # Needed for GoReleaser to create the GitHub Release and upload assets.
10+ contents: write
11+
12+jobs:
13+ goreleaser:
14+ runs-on: ubuntu-latest
15+ steps:
16+ - name: checkout
17+ uses: actions/checkout@v4
18+ with:
19+ # GoReleaser needs the full history to build a useful
20+ # changelog from commit messages.
21+ fetch-depth: 0
22+
23+ - name: set up go
24+ uses: actions/setup-go@v5
25+ with:
26+ go-version: "1.23"
27+ cache: true
28+
29+ - name: run goreleaser
30+ uses: goreleaser/goreleaser-action@v6
31+ with:
32+ distribution: goreleaser
33+ version: latest
34+ args: release --clean
35+ env:
36+ # Default token for the release itself (uploads artifacts,
37+ # creates the release page).
38+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+ # Optional: PAT with `repo` scope for patriceckhart/homebrew-tap
40+ # so we can push a Formula/zot.rb update on every release.
41+ # Create the secret in repo settings → actions → secrets.
42+ # If unset, goreleaser skips the brew step (skip_upload: auto).
43+ HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
@@ -0,0 +1,43 @@
1+name: release
2+
3+on:
4+ push:
5+ tags:
6+ - "v*"
7+
8+permissions:
9+ # Needed for GoReleaser to create the GitHub Release and upload assets.
10+ contents: write
11+
12+jobs:
13+ goreleaser:
14+ runs-on: ubuntu-latest
15+ steps:
16+ - name: checkout
17+ uses: actions/checkout@v4
18+ with:
19+ # GoReleaser needs the full history to build a useful
20+ # changelog from commit messages.
21+ fetch-depth: 0
22+
23+ - name: set up go
24+ uses: actions/setup-go@v5
25+ with:
26+ go-version: "1.23"
27+ cache: true
28+
29+ - name: run goreleaser
30+ uses: goreleaser/goreleaser-action@v6
31+ with:
32+ distribution: goreleaser
33+ version: latest
34+ args: release --clean
35+ env:
36+ # Default token for the release itself (uploads artifacts,
37+ # creates the release page).
38+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+ # Optional: PAT with `repo` scope for patriceckhart/homebrew-tap
40+ # so we can push a Formula/zot.rb update on every release.
41+ # Create the secret in repo settings → actions → secrets.
42+ # If unset, goreleaser skips the brew step (skip_upload: auto).
43+ HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
A .goreleaser.yaml +122 -0
@@ -0,0 +1,122 @@
1+# GoReleaser config — builds cross-platform binaries and GitHub Release
2+# artifacts for zot.
3+#
4+# Triggered by pushing a tag like `v0.1.0`. The release workflow runs
5+# `goreleaser release --clean`, which compiles every target, packs
6+# them into `.tar.gz` (Unix) / `.zip` (Windows) archives, generates a
7+# checksum file, and uploads everything to a GitHub Release.
8+
9+version: 2
10+
11+project_name: zot
12+
13+before:
14+ hooks:
15+ - go mod tidy
16+
17+builds:
18+ - id: zot
19+ main: ./cmd/zot
20+ binary: zot
21+ env:
22+ - CGO_ENABLED=0
23+ goos: [linux, darwin, windows]
24+ goarch: [amd64, arm64]
25+ # Windows/arm64 is shipped by Go but rarely used — skip to keep the
26+ # release artifact list tidy.
27+ ignore:
28+ - goos: windows
29+ goarch: arm64
30+ flags:
31+ - -trimpath
32+ ldflags:
33+ - -s -w
34+ - -X main.version={{.Version}}
35+ - -X main.commit={{.Commit}}
36+ - -X main.date={{.Date}}
37+
38+archives:
39+ - id: zot
40+ # Human-friendly name: zot_0.1.0_darwin_arm64.tar.gz
41+ name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
42+ files:
43+ - LICENSE
44+ - README.md
45+ format_overrides:
46+ - goos: windows
47+ format: zip
48+
49+checksum:
50+ name_template: "checksums.txt"
51+ algorithm: sha256
52+
53+snapshot:
54+ # Used by `goreleaser release --snapshot`; lets contributors build
55+ # a local release without pushing a tag.
56+ version_template: "{{ incpatch .Version }}-next"
57+
58+changelog:
59+ sort: asc
60+ use: github
61+ filters:
62+ exclude:
63+ - "^docs:"
64+ - "^chore:"
65+ - "^ci:"
66+ - "^test:"
67+ groups:
68+ - title: features
69+ regexp: "^.*feat[(\\w)]*:+.*$"
70+ order: 0
71+ - title: fixes
72+ regexp: "^.*fix[(\\w)]*:+.*$"
73+ order: 1
74+ - title: other
75+ order: 999
76+
77+release:
78+ github:
79+ owner: patriceckhart
80+ name: zot
81+ draft: false
82+ prerelease: auto
83+ # Auto-generated release notes include the changelog groups above plus
84+ # an install snippet so the release page itself is a landing page.
85+ header: |
86+ ## zot {{ .Tag }}
87+
88+ one-liner install:
89+
90+ ```bash
91+ curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash
92+ ```
93+
94+ homebrew:
95+
96+ ```bash
97+ brew install patriceckhart/tap/zot
98+ ```
99+
100+ or download a binary below, `chmod +x`, and drop it on your `$PATH`.
101+
102+# Optional: publish a Homebrew formula to a tap repo on every release.
103+# Requires the tap repo to exist at github.com/patriceckhart/homebrew-tap
104+# and a PAT with `repo` scope exported as `HOMEBREW_TAP_TOKEN` in the
105+# release workflow. Safe to leave enabled even before the tap exists;
106+# goreleaser skips it when the token isn't set.
107+brews:
108+ - repository:
109+ owner: patriceckhart
110+ name: homebrew-tap
111+ token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
112+ name: zot
113+ homepage: https://github.com/patriceckhart/zot
114+ description: "lightweight coding agent harness — anthropic + openai, four tools, tui"
115+ license: MIT
116+ install: |
117+ bin.install "zot"
118+ test: |
119+ system "#{bin}/zot", "--help"
120+ # Only publish the formula when the token is actually set, so
121+ # ordinary tag pushes from forks don't explode.
122+ skip_upload: auto
@@ -0,0 +1,122 @@
1+# GoReleaser config — builds cross-platform binaries and GitHub Release
2+# artifacts for zot.
3+#
4+# Triggered by pushing a tag like `v0.1.0`. The release workflow runs
5+# `goreleaser release --clean`, which compiles every target, packs
6+# them into `.tar.gz` (Unix) / `.zip` (Windows) archives, generates a
7+# checksum file, and uploads everything to a GitHub Release.
8+
9+version: 2
10+
11+project_name: zot
12+
13+before:
14+ hooks:
15+ - go mod tidy
16+
17+builds:
18+ - id: zot
19+ main: ./cmd/zot
20+ binary: zot
21+ env:
22+ - CGO_ENABLED=0
23+ goos: [linux, darwin, windows]
24+ goarch: [amd64, arm64]
25+ # Windows/arm64 is shipped by Go but rarely used — skip to keep the
26+ # release artifact list tidy.
27+ ignore:
28+ - goos: windows
29+ goarch: arm64
30+ flags:
31+ - -trimpath
32+ ldflags:
33+ - -s -w
34+ - -X main.version={{.Version}}
35+ - -X main.commit={{.Commit}}
36+ - -X main.date={{.Date}}
37+
38+archives:
39+ - id: zot
40+ # Human-friendly name: zot_0.1.0_darwin_arm64.tar.gz
41+ name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
42+ files:
43+ - LICENSE
44+ - README.md
45+ format_overrides:
46+ - goos: windows
47+ format: zip
48+
49+checksum:
50+ name_template: "checksums.txt"
51+ algorithm: sha256
52+
53+snapshot:
54+ # Used by `goreleaser release --snapshot`; lets contributors build
55+ # a local release without pushing a tag.
56+ version_template: "{{ incpatch .Version }}-next"
57+
58+changelog:
59+ sort: asc
60+ use: github
61+ filters:
62+ exclude:
63+ - "^docs:"
64+ - "^chore:"
65+ - "^ci:"
66+ - "^test:"
67+ groups:
68+ - title: features
69+ regexp: "^.*feat[(\\w)]*:+.*$"
70+ order: 0
71+ - title: fixes
72+ regexp: "^.*fix[(\\w)]*:+.*$"
73+ order: 1
74+ - title: other
75+ order: 999
76+
77+release:
78+ github:
79+ owner: patriceckhart
80+ name: zot
81+ draft: false
82+ prerelease: auto
83+ # Auto-generated release notes include the changelog groups above plus
84+ # an install snippet so the release page itself is a landing page.
85+ header: |
86+ ## zot {{ .Tag }}
87+
88+ one-liner install:
89+
90+ ```bash
91+ curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash
92+ ```
93+
94+ homebrew:
95+
96+ ```bash
97+ brew install patriceckhart/tap/zot
98+ ```
99+
100+ or download a binary below, `chmod +x`, and drop it on your `$PATH`.
101+
102+# Optional: publish a Homebrew formula to a tap repo on every release.
103+# Requires the tap repo to exist at github.com/patriceckhart/homebrew-tap
104+# and a PAT with `repo` scope exported as `HOMEBREW_TAP_TOKEN` in the
105+# release workflow. Safe to leave enabled even before the tap exists;
106+# goreleaser skips it when the token isn't set.
107+brews:
108+ - repository:
109+ owner: patriceckhart
110+ name: homebrew-tap
111+ token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
112+ name: zot
113+ homepage: https://github.com/patriceckhart/zot
114+ description: "lightweight coding agent harness — anthropic + openai, four tools, tui"
115+ license: MIT
116+ install: |
117+ bin.install "zot"
118+ test: |
119+ system "#{bin}/zot", "--help"
120+ # Only publish the formula when the token is actually set, so
121+ # ordinary tag pushes from forks don't explode.
122+ skip_upload: auto
M Makefile +3 -1
@@ -1,4 +1,6 @@
1-VERSION ?= dev
1+# Local / untagged builds ship as 0.0.0. Release builds are driven by
2+# goreleaser which overrides VERSION from the git tag.
3+VERSION ?= 0.0.0
24 LDFLAGS := -s -w -X main.version=$(VERSION)
35
46 .PHONY: build install test lint fmt clean release
@@ -1,4 +1,6 @@
1-VERSION ?= dev1+# Local / untagged builds ship as 0.0.0. Release builds are driven by
2+# goreleaser which overrides VERSION from the git tag.
3+VERSION ?= 0.0.0
2 LDFLAGS := -s -w -X main.version=$(VERSION)4 LDFLAGS := -s -w -X main.version=$(VERSION)
3 5
4 .PHONY: build install test lint fmt clean release6 .PHONY: build install test lint fmt clean release
M README.md +33 -1
@@ -12,11 +12,39 @@ yet another coding agent harness, lightweight and written (vibe-slopped) in go.
1212
1313 ## install
1414
15+### one-liner (macos + linux)
16+
17+```bash
18+curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash
19+```
20+
21+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:
22+
23+```bash
24+curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash -s -- v0.0.1 ~/bin
25+```
26+
27+### one-liner (windows, powershell)
28+
29+```powershell
30+iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex
31+```
32+
33+drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. open a fresh terminal afterwards.
34+
35+### homebrew (macos + linux)
36+
37+```bash
38+brew install patriceckhart/tap/zot
39+```
40+
41+### go install
42+
1543 ```bash
1644 go install github.com/patriceckhart/zot/cmd/zot@latest
1745 ```
1846
19-or from source:
47+### from source
2048
2149 ```bash
2250 git clone https://github.com/patriceckhart/zot
@@ -25,6 +53,10 @@ make build # produces ./bin/zot
2553 make install # into $GOPATH/bin
2654 ```
2755
56+### prebuilt binaries
57+
58+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`.
59+
2860 ## authenticate
2961
3062 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.
@@ -12,11 +12,39 @@ yet another coding agent harness, lightweight and written (vibe-slopped) in go.
12 12
13 ## install13 ## install
14 14
15+### one-liner (macos + linux)
16+
17+```bash
18+curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash
19+```
20+
21+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:
22+
23+```bash
24+curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash -s -- v0.0.1 ~/bin
25+```
26+
27+### one-liner (windows, powershell)
28+
29+```powershell
30+iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex
31+```
32+
33+drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. open a fresh terminal afterwards.
34+
35+### homebrew (macos + linux)
36+
37+```bash
38+brew install patriceckhart/tap/zot
39+```
40+
41+### go install
42+
15 ```bash43 ```bash
16 go install github.com/patriceckhart/zot/cmd/zot@latest44 go install github.com/patriceckhart/zot/cmd/zot@latest
17 ```45 ```
18 46
19-or from source:47+### from source
20 48
21 ```bash49 ```bash
22 git clone https://github.com/patriceckhart/zot50 git clone https://github.com/patriceckhart/zot
@@ -25,6 +53,10 @@ make build # produces ./bin/zot
25 make install # into $GOPATH/bin53 make install # into $GOPATH/bin
26 ```54 ```
27 55
56+### prebuilt binaries
57+
58+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`.
59+
28 ## authenticate60 ## authenticate
29 61
30 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.62 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.
M cmd/zot/main.go +25 -2
@@ -8,10 +8,33 @@ import (
88 "github.com/patriceckhart/zot/internal/agent"
99 )
1010
11-var version = "dev"
11+// Injected at build time via -ldflags "-X main.version=... -X main.commit=... -X main.date=...".
12+// See .goreleaser.yaml for the release build and the Makefile for
13+// local builds. Defaults make `zot --version` print something sensible
14+// when built without ldflags.
15+var (
16+ // 0.0.0 is the pre-release placeholder for local / untagged
17+ // builds. The first published GitHub release will be tagged
18+ // v0.0.1; everything before that ships as 0.0.0 from source.
19+ version = "0.0.0"
20+ commit = ""
21+ date = ""
22+)
1223
1324 func main() {
14- if err := agent.Run(os.Args[1:], version); err != nil {
25+ v := version
26+ if commit != "" {
27+ short := commit
28+ if len(short) > 7 {
29+ short = short[:7]
30+ }
31+ v = v + " (" + short
32+ if date != "" {
33+ v = v + ", " + date
34+ }
35+ v = v + ")"
36+ }
37+ if err := agent.Run(os.Args[1:], v); err != nil {
1538 fmt.Fprintln(os.Stderr, "zot:", err)
1639 os.Exit(1)
1740 }
@@ -8,10 +8,33 @@ import (
8 "github.com/patriceckhart/zot/internal/agent"8 "github.com/patriceckhart/zot/internal/agent"
9 )9 )
10 10
11-var version = "dev"11+// Injected at build time via -ldflags "-X main.version=... -X main.commit=... -X main.date=...".
12+// See .goreleaser.yaml for the release build and the Makefile for
13+// local builds. Defaults make `zot --version` print something sensible
14+// when built without ldflags.
15+var (
16+ // 0.0.0 is the pre-release placeholder for local / untagged
17+ // builds. The first published GitHub release will be tagged
18+ // v0.0.1; everything before that ships as 0.0.0 from source.
19+ version = "0.0.0"
20+ commit = ""
21+ date = ""
22+)
12 23
13 func main() {24 func main() {
14- if err := agent.Run(os.Args[1:], version); err != nil {25+ v := version
26+ if commit != "" {
27+ short := commit
28+ if len(short) > 7 {
29+ short = short[:7]
30+ }
31+ v = v + " (" + short
32+ if date != "" {
33+ v = v + ", " + date
34+ }
35+ v = v + ")"
36+ }
37+ if err := agent.Run(os.Args[1:], v); err != nil {
15 fmt.Fprintln(os.Stderr, "zot:", err)38 fmt.Fprintln(os.Stderr, "zot:", err)
16 os.Exit(1)39 os.Exit(1)
17 }40 }
A install.ps1 +115 -0
@@ -0,0 +1,115 @@
1+# zot installer for Windows (PowerShell).
2+#
3+# Usage (in PowerShell):
4+# iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex
5+#
6+# Or with arguments:
7+# $env:ZOT_VERSION = "v0.0.1"
8+# $env:ZOT_PREFIX = "$HOME\bin"
9+# iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex
10+#
11+# Detects architecture, downloads the matching .zip from the GitHub
12+# release, verifies the sha256 against checksums.txt, extracts zot.exe,
13+# and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH
14+# via the User environment if missing).
15+
16+[CmdletBinding()]
17+param(
18+ [string]$Version = $env:ZOT_VERSION,
19+ [string]$Prefix = $env:ZOT_PREFIX
20+)
21+
22+$ErrorActionPreference = "Stop"
23+
24+$owner = "patriceckhart"
25+$repo = "zot"
26+$binary = "zot"
27+
28+if (-not $Version) { $Version = "latest" }
29+if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" }
30+
31+function Msg($m) { Write-Host "==> $m" -ForegroundColor Cyan }
32+function Warn($m) { Write-Warning $m }
33+function Die($m) { Write-Error $m; exit 1 }
34+
35+# ---- detect architecture ----
36+
37+switch -wildcard ($env:PROCESSOR_ARCHITECTURE) {
38+ "AMD64" { $arch = "amd64" }
39+ "ARM64" { $arch = "arm64" }
40+ default { Die "unsupported arch: $($env:PROCESSOR_ARCHITECTURE)" }
41+}
42+
43+# ARM64 Windows isn't shipped (see .goreleaser.yaml ignore rule) — fall
44+# back to amd64 which runs fine under ARM64 emulation.
45+if ($arch -eq "arm64") {
46+ Warn "windows/arm64 is not published; falling back to amd64"
47+ $arch = "amd64"
48+}
49+
50+# ---- resolve version ----
51+
52+if ($Version -eq "latest") {
53+ $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 `
54+ "https://github.com/$owner/$repo/releases/latest"
55+ if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') {
56+ $Version = $Matches[1]
57+ } else {
58+ Die "could not resolve latest version"
59+ }
60+}
61+
62+if (-not $Version.StartsWith("v")) { $Version = "v$Version" }
63+$verNum = $Version.TrimStart("v")
64+
65+# ---- download + verify + extract ----
66+
67+$archive = "${binary}_${verNum}_windows_${arch}.zip"
68+$baseUrl = "https://github.com/$owner/$repo/releases/download/$Version"
69+$archiveUrl = "$baseUrl/$archive"
70+$checksumUrl = "$baseUrl/checksums.txt"
71+
72+$tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("zot-install-" + [System.Guid]::NewGuid().ToString("N").Substring(0,8)))
73+
74+try {
75+ Msg "downloading $archive"
76+ Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)
77+
78+ Msg "verifying checksum"
79+ $checksums = Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl | Select-Object -ExpandProperty Content
80+ $expected = ($checksums -split "`n" | Where-Object { $_ -match [regex]::Escape($archive) + "$" } | Select-Object -First 1)
81+ if (-not $expected) { Die "no checksum for $archive in checksums.txt" }
82+ $expectedHash = ($expected -split "\s+")[0]
83+
84+ $actualHash = (Get-FileHash -Path (Join-Path $tmp $archive) -Algorithm SHA256).Hash.ToLower()
85+ if ($expectedHash.ToLower() -ne $actualHash) {
86+ Die "checksum mismatch: expected $expectedHash, got $actualHash"
87+ }
88+
89+ Msg "extracting"
90+ Expand-Archive -Path (Join-Path $tmp $archive) -DestinationPath $tmp -Force
91+
92+ $exe = Join-Path $tmp "$binary.exe"
93+ if (-not (Test-Path $exe)) { Die "archive did not contain $binary.exe" }
94+
95+ Msg "installing to $Prefix\$binary.exe"
96+ New-Item -ItemType Directory -Path $Prefix -Force | Out-Null
97+ Copy-Item $exe (Join-Path $Prefix "$binary.exe") -Force
98+
99+ # ---- PATH hint ----
100+
101+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
102+ $parts = $userPath -split ";" | Where-Object { $_ }
103+ if (-not ($parts -contains $Prefix)) {
104+ Warn "$Prefix is not on your user PATH"
105+ Warn "adding it for future sessions..."
106+ [Environment]::SetEnvironmentVariable("Path", ($userPath.TrimEnd(";") + ";" + $Prefix), "User")
107+ Warn "open a new terminal to pick up the change, or run:"
108+ Warn " `$env:Path = `"$Prefix;`$env:Path`""
109+ }
110+
111+ Msg "installed. run: zot --help"
112+}
113+finally {
114+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
115+}
@@ -0,0 +1,115 @@
1+# zot installer for Windows (PowerShell).
2+#
3+# Usage (in PowerShell):
4+# iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex
5+#
6+# Or with arguments:
7+# $env:ZOT_VERSION = "v0.0.1"
8+# $env:ZOT_PREFIX = "$HOME\bin"
9+# iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex
10+#
11+# Detects architecture, downloads the matching .zip from the GitHub
12+# release, verifies the sha256 against checksums.txt, extracts zot.exe,
13+# and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH
14+# via the User environment if missing).
15+
16+[CmdletBinding()]
17+param(
18+ [string]$Version = $env:ZOT_VERSION,
19+ [string]$Prefix = $env:ZOT_PREFIX
20+)
21+
22+$ErrorActionPreference = "Stop"
23+
24+$owner = "patriceckhart"
25+$repo = "zot"
26+$binary = "zot"
27+
28+if (-not $Version) { $Version = "latest" }
29+if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" }
30+
31+function Msg($m) { Write-Host "==> $m" -ForegroundColor Cyan }
32+function Warn($m) { Write-Warning $m }
33+function Die($m) { Write-Error $m; exit 1 }
34+
35+# ---- detect architecture ----
36+
37+switch -wildcard ($env:PROCESSOR_ARCHITECTURE) {
38+ "AMD64" { $arch = "amd64" }
39+ "ARM64" { $arch = "arm64" }
40+ default { Die "unsupported arch: $($env:PROCESSOR_ARCHITECTURE)" }
41+}
42+
43+# ARM64 Windows isn't shipped (see .goreleaser.yaml ignore rule) — fall
44+# back to amd64 which runs fine under ARM64 emulation.
45+if ($arch -eq "arm64") {
46+ Warn "windows/arm64 is not published; falling back to amd64"
47+ $arch = "amd64"
48+}
49+
50+# ---- resolve version ----
51+
52+if ($Version -eq "latest") {
53+ $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 `
54+ "https://github.com/$owner/$repo/releases/latest"
55+ if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') {
56+ $Version = $Matches[1]
57+ } else {
58+ Die "could not resolve latest version"
59+ }
60+}
61+
62+if (-not $Version.StartsWith("v")) { $Version = "v$Version" }
63+$verNum = $Version.TrimStart("v")
64+
65+# ---- download + verify + extract ----
66+
67+$archive = "${binary}_${verNum}_windows_${arch}.zip"
68+$baseUrl = "https://github.com/$owner/$repo/releases/download/$Version"
69+$archiveUrl = "$baseUrl/$archive"
70+$checksumUrl = "$baseUrl/checksums.txt"
71+
72+$tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("zot-install-" + [System.Guid]::NewGuid().ToString("N").Substring(0,8)))
73+
74+try {
75+ Msg "downloading $archive"
76+ Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile (Join-Path $tmp $archive)
77+
78+ Msg "verifying checksum"
79+ $checksums = Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl | Select-Object -ExpandProperty Content
80+ $expected = ($checksums -split "`n" | Where-Object { $_ -match [regex]::Escape($archive) + "$" } | Select-Object -First 1)
81+ if (-not $expected) { Die "no checksum for $archive in checksums.txt" }
82+ $expectedHash = ($expected -split "\s+")[0]
83+
84+ $actualHash = (Get-FileHash -Path (Join-Path $tmp $archive) -Algorithm SHA256).Hash.ToLower()
85+ if ($expectedHash.ToLower() -ne $actualHash) {
86+ Die "checksum mismatch: expected $expectedHash, got $actualHash"
87+ }
88+
89+ Msg "extracting"
90+ Expand-Archive -Path (Join-Path $tmp $archive) -DestinationPath $tmp -Force
91+
92+ $exe = Join-Path $tmp "$binary.exe"
93+ if (-not (Test-Path $exe)) { Die "archive did not contain $binary.exe" }
94+
95+ Msg "installing to $Prefix\$binary.exe"
96+ New-Item -ItemType Directory -Path $Prefix -Force | Out-Null
97+ Copy-Item $exe (Join-Path $Prefix "$binary.exe") -Force
98+
99+ # ---- PATH hint ----
100+
101+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
102+ $parts = $userPath -split ";" | Where-Object { $_ }
103+ if (-not ($parts -contains $Prefix)) {
104+ Warn "$Prefix is not on your user PATH"
105+ Warn "adding it for future sessions..."
106+ [Environment]::SetEnvironmentVariable("Path", ($userPath.TrimEnd(";") + ";" + $Prefix), "User")
107+ Warn "open a new terminal to pick up the change, or run:"
108+ Warn " `$env:Path = `"$Prefix;`$env:Path`""
109+ }
110+
111+ Msg "installed. run: zot --help"
112+}
113+finally {
114+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
115+}
A install.sh +156 -0
@@ -0,0 +1,156 @@
1+#!/usr/bin/env bash
2+#
3+# zot installer.
4+#
5+# Usage:
6+# curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash
7+# curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash -s -- v0.0.1 ~/bin
8+#
9+# Positional arguments:
10+# $1 version — release tag (e.g. v0.0.1). Defaults to "latest".
11+# $2 prefix — install directory. Defaults to the first writable
12+# directory in: /usr/local/bin, $HOME/.local/bin,
13+# $HOME/bin. Created if missing. Add it to your PATH
14+# if it isn't already.
15+#
16+# Environment overrides:
17+# ZOT_VERSION same as $1
18+# ZOT_PREFIX same as $2
19+#
20+# The script detects your OS and architecture, downloads the matching
21+# archive from the GitHub release, verifies the sha256 against the
22+# release's checksums.txt, extracts the binary, and moves it into the
23+# prefix directory. No sudo unless you explicitly pick a prefix that
24+# needs it.
25+
26+set -euo pipefail
27+
28+OWNER="patriceckhart"
29+REPO="zot"
30+BINARY="zot"
31+
32+VERSION="${1:-${ZOT_VERSION:-latest}}"
33+PREFIX="${2:-${ZOT_PREFIX:-}}"
34+
35+msg() { printf "\033[1m==>\033[0m %s\n" "$*"; }
36+warn() { printf "\033[33mwarn:\033[0m %s\n" "$*" >&2; }
37+die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; }
38+
39+command -v curl >/dev/null 2>&1 || die "curl is required"
40+command -v tar >/dev/null 2>&1 || die "tar is required"
41+
42+# ---- detect OS + arch ----
43+
44+uname_s=$(uname -s)
45+uname_m=$(uname -m)
46+
47+case "$uname_s" in
48+ Linux) OS=linux ;;
49+ Darwin) OS=darwin ;;
50+ MINGW*|MSYS*|CYGWIN*)
51+ die "windows detected — use install.ps1 from powershell instead"
52+ ;;
53+ *) die "unsupported os: $uname_s" ;;
54+esac
55+
56+case "$uname_m" in
57+ x86_64|amd64) ARCH=amd64 ;;
58+ arm64|aarch64) ARCH=arm64 ;;
59+ *) die "unsupported arch: $uname_m" ;;
60+esac
61+
62+# ---- resolve version ----
63+
64+if [ "$VERSION" = "latest" ]; then
65+ # GitHub's /releases/latest endpoint redirects to /releases/tag/<tag>.
66+ # We follow the redirect and grab the tag from the final URL without
67+ # needing jq.
68+ VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
69+ "https://github.com/${OWNER}/${REPO}/releases/latest" \
70+ | sed -E 's|.*/tag/([^/]+).*|\1|')
71+ [ -n "$VERSION" ] || die "could not resolve latest version"
72+fi
73+
74+case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac
75+VER_NUM="${VERSION#v}"
76+
77+# ---- pick an install prefix ----
78+
79+pick_prefix() {
80+ local candidates=()
81+ [ -n "$PREFIX" ] && { echo "$PREFIX"; return; }
82+ candidates+=("/usr/local/bin")
83+ [ -n "${HOME:-}" ] && candidates+=("$HOME/.local/bin" "$HOME/bin")
84+ for d in "${candidates[@]}"; do
85+ if [ -d "$d" ] && [ -w "$d" ]; then
86+ echo "$d"
87+ return
88+ fi
89+ done
90+ # Nothing writable yet — create ~/.local/bin and use that.
91+ if [ -n "${HOME:-}" ]; then
92+ mkdir -p "$HOME/.local/bin"
93+ echo "$HOME/.local/bin"
94+ return
95+ fi
96+ die "no writable install prefix found; pass one as the second argument"
97+}
98+
99+PREFIX=$(pick_prefix)
100+mkdir -p "$PREFIX"
101+
102+# ---- download + verify + extract ----
103+
104+ARCHIVE="${BINARY}_${VER_NUM}_${OS}_${ARCH}.tar.gz"
105+BASE_URL="https://github.com/${OWNER}/${REPO}/releases/download/${VERSION}"
106+ARCHIVE_URL="${BASE_URL}/${ARCHIVE}"
107+CHECKSUMS_URL="${BASE_URL}/checksums.txt"
108+
109+TMP=$(mktemp -d)
110+trap 'rm -rf "$TMP"' EXIT
111+
112+msg "downloading ${ARCHIVE}"
113+curl -fsSL -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \
114+ || die "download failed: $ARCHIVE_URL"
115+
116+msg "verifying checksum"
117+curl -fsSL -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \
118+ || die "download failed: $CHECKSUMS_URL"
119+
120+expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true)
121+[ -n "$expected" ] || die "no checksum for $ARCHIVE in checksums.txt"
122+
123+if command -v sha256sum >/dev/null 2>&1; then
124+ actual=$(sha256sum "$TMP/$ARCHIVE" | awk '{print $1}')
125+elif command -v shasum >/dev/null 2>&1; then
126+ actual=$(shasum -a 256 "$TMP/$ARCHIVE" | awk '{print $1}')
127+else
128+ die "no sha256 tool found (sha256sum or shasum)"
129+fi
130+
131+[ "$expected" = "$actual" ] \
132+ || die "checksum mismatch: expected $expected, got $actual"
133+
134+msg "extracting"
135+tar -xzf "$TMP/$ARCHIVE" -C "$TMP"
136+
137+[ -f "$TMP/$BINARY" ] || die "archive did not contain a '$BINARY' binary"
138+
139+msg "installing to $PREFIX/$BINARY"
140+install -m 0755 "$TMP/$BINARY" "$PREFIX/$BINARY" 2>/dev/null \
141+ || { cp "$TMP/$BINARY" "$PREFIX/$BINARY" && chmod 0755 "$PREFIX/$BINARY"; }
142+
143+# ---- PATH hint ----
144+
145+case ":$PATH:" in
146+ *":$PREFIX:"*) ;;
147+ *)
148+ warn "$PREFIX is not on your PATH"
149+ warn "add this to your shell rc file:"
150+ warn " export PATH=\"$PREFIX:\$PATH\""
151+ ;;
152+esac
153+
154+msg "installed $("$PREFIX/$BINARY" --version 2>/dev/null || echo zot)"
155+msg "run: zot (interactive tui)"
156+msg "run: zot --help (all flags and subcommands)"
@@ -0,0 +1,156 @@
1+#!/usr/bin/env bash
2+#
3+# zot installer.
4+#
5+# Usage:
6+# curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash
7+# curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash -s -- v0.0.1 ~/bin
8+#
9+# Positional arguments:
10+# $1 version — release tag (e.g. v0.0.1). Defaults to "latest".
11+# $2 prefix — install directory. Defaults to the first writable
12+# directory in: /usr/local/bin, $HOME/.local/bin,
13+# $HOME/bin. Created if missing. Add it to your PATH
14+# if it isn't already.
15+#
16+# Environment overrides:
17+# ZOT_VERSION same as $1
18+# ZOT_PREFIX same as $2
19+#
20+# The script detects your OS and architecture, downloads the matching
21+# archive from the GitHub release, verifies the sha256 against the
22+# release's checksums.txt, extracts the binary, and moves it into the
23+# prefix directory. No sudo unless you explicitly pick a prefix that
24+# needs it.
25+
26+set -euo pipefail
27+
28+OWNER="patriceckhart"
29+REPO="zot"
30+BINARY="zot"
31+
32+VERSION="${1:-${ZOT_VERSION:-latest}}"
33+PREFIX="${2:-${ZOT_PREFIX:-}}"
34+
35+msg() { printf "\033[1m==>\033[0m %s\n" "$*"; }
36+warn() { printf "\033[33mwarn:\033[0m %s\n" "$*" >&2; }
37+die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; }
38+
39+command -v curl >/dev/null 2>&1 || die "curl is required"
40+command -v tar >/dev/null 2>&1 || die "tar is required"
41+
42+# ---- detect OS + arch ----
43+
44+uname_s=$(uname -s)
45+uname_m=$(uname -m)
46+
47+case "$uname_s" in
48+ Linux) OS=linux ;;
49+ Darwin) OS=darwin ;;
50+ MINGW*|MSYS*|CYGWIN*)
51+ die "windows detected — use install.ps1 from powershell instead"
52+ ;;
53+ *) die "unsupported os: $uname_s" ;;
54+esac
55+
56+case "$uname_m" in
57+ x86_64|amd64) ARCH=amd64 ;;
58+ arm64|aarch64) ARCH=arm64 ;;
59+ *) die "unsupported arch: $uname_m" ;;
60+esac
61+
62+# ---- resolve version ----
63+
64+if [ "$VERSION" = "latest" ]; then
65+ # GitHub's /releases/latest endpoint redirects to /releases/tag/<tag>.
66+ # We follow the redirect and grab the tag from the final URL without
67+ # needing jq.
68+ VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
69+ "https://github.com/${OWNER}/${REPO}/releases/latest" \
70+ | sed -E 's|.*/tag/([^/]+).*|\1|')
71+ [ -n "$VERSION" ] || die "could not resolve latest version"
72+fi
73+
74+case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac
75+VER_NUM="${VERSION#v}"
76+
77+# ---- pick an install prefix ----
78+
79+pick_prefix() {
80+ local candidates=()
81+ [ -n "$PREFIX" ] && { echo "$PREFIX"; return; }
82+ candidates+=("/usr/local/bin")
83+ [ -n "${HOME:-}" ] && candidates+=("$HOME/.local/bin" "$HOME/bin")
84+ for d in "${candidates[@]}"; do
85+ if [ -d "$d" ] && [ -w "$d" ]; then
86+ echo "$d"
87+ return
88+ fi
89+ done
90+ # Nothing writable yet — create ~/.local/bin and use that.
91+ if [ -n "${HOME:-}" ]; then
92+ mkdir -p "$HOME/.local/bin"
93+ echo "$HOME/.local/bin"
94+ return
95+ fi
96+ die "no writable install prefix found; pass one as the second argument"
97+}
98+
99+PREFIX=$(pick_prefix)
100+mkdir -p "$PREFIX"
101+
102+# ---- download + verify + extract ----
103+
104+ARCHIVE="${BINARY}_${VER_NUM}_${OS}_${ARCH}.tar.gz"
105+BASE_URL="https://github.com/${OWNER}/${REPO}/releases/download/${VERSION}"
106+ARCHIVE_URL="${BASE_URL}/${ARCHIVE}"
107+CHECKSUMS_URL="${BASE_URL}/checksums.txt"
108+
109+TMP=$(mktemp -d)
110+trap 'rm -rf "$TMP"' EXIT
111+
112+msg "downloading ${ARCHIVE}"
113+curl -fsSL -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \
114+ || die "download failed: $ARCHIVE_URL"
115+
116+msg "verifying checksum"
117+curl -fsSL -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \
118+ || die "download failed: $CHECKSUMS_URL"
119+
120+expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true)
121+[ -n "$expected" ] || die "no checksum for $ARCHIVE in checksums.txt"
122+
123+if command -v sha256sum >/dev/null 2>&1; then
124+ actual=$(sha256sum "$TMP/$ARCHIVE" | awk '{print $1}')
125+elif command -v shasum >/dev/null 2>&1; then
126+ actual=$(shasum -a 256 "$TMP/$ARCHIVE" | awk '{print $1}')
127+else
128+ die "no sha256 tool found (sha256sum or shasum)"
129+fi
130+
131+[ "$expected" = "$actual" ] \
132+ || die "checksum mismatch: expected $expected, got $actual"
133+
134+msg "extracting"
135+tar -xzf "$TMP/$ARCHIVE" -C "$TMP"
136+
137+[ -f "$TMP/$BINARY" ] || die "archive did not contain a '$BINARY' binary"
138+
139+msg "installing to $PREFIX/$BINARY"
140+install -m 0755 "$TMP/$BINARY" "$PREFIX/$BINARY" 2>/dev/null \
141+ || { cp "$TMP/$BINARY" "$PREFIX/$BINARY" && chmod 0755 "$PREFIX/$BINARY"; }
142+
143+# ---- PATH hint ----
144+
145+case ":$PATH:" in
146+ *":$PREFIX:"*) ;;
147+ *)
148+ warn "$PREFIX is not on your PATH"
149+ warn "add this to your shell rc file:"
150+ warn " export PATH=\"$PREFIX:\$PATH\""
151+ ;;
152+esac
153+
154+msg "installed $("$PREFIX/$BINARY" --version 2>/dev/null || echo zot)"
155+msg "run: zot (interactive tui)"
156+msg "run: zot --help (all flags and subcommands)"

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close