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 | |
| 2 | 4 | LDFLAGS := -s -w -X main.version=$(VERSION) | |
| 3 | 5 | ||
| 4 | 6 | .PHONY: build install test lint fmt clean release | |
| @@ -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 | |||
| 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 release | 6 | .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. | |||
| 12 | 12 | ||
| 13 | 13 | ## 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 | 43 | ```bash | |
| 16 | 44 | go install github.com/patriceckhart/zot/cmd/zot@latest | |
| 17 | 45 | ``` | |
| 18 | 46 | ||
| 19 | - | or from source: | |
| 47 | + | ### from source | |
| 20 | 48 | ||
| 21 | 49 | ```bash | |
| 22 | 50 | git clone https://github.com/patriceckhart/zot | |
| @@ -25,6 +53,10 @@ make build # produces ./bin/zot | |||
| 25 | 53 | 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 | 60 | ## authenticate | |
| 29 | 61 | ||
| 30 | 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. | |
| @@ -12,11 +12,39 @@ yet another coding agent harness, lightweight and written (vibe-slopped) in go. | |||||
| 12 | 12 | ||||
| 13 | ## install | 13 | ## 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 | ```bash | 43 | ```bash | ||
| 16 | go install github.com/patriceckhart/zot/cmd/zot@latest | 44 | go install github.com/patriceckhart/zot/cmd/zot@latest | ||
| 17 | ``` | 45 | ``` | ||
| 18 | 46 | ||||
| 19 | - | or from source: | 47 | + | ### from source |
| 20 | 48 | ||||
| 21 | ```bash | 49 | ```bash | ||
| 22 | git clone https://github.com/patriceckhart/zot | 50 | git clone https://github.com/patriceckhart/zot | ||
| @@ -25,6 +53,10 @@ make build # produces ./bin/zot | |||||
| 25 | make install # into $GOPATH/bin | 53 | 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 | ## authenticate | 60 | ## 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 ( | |||
| 8 | 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 | 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 | 38 | fmt.Fprintln(os.Stderr, "zot:", err) | |
| 16 | 39 | os.Exit(1) | |
| 17 | 40 | } | |
| @@ -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)" | |||