fix ci: portable syscall.Select via x/sys/unix; gofmt pass
- rewrite resize_unix.go on top of golang.org/x/sys/unix so the peek-stdin helper compiles on linux (Select returns (int, error), Timeval.Usec is int64) as well as darwin (int32, error-only) - promote golang.org/x/sys to a direct dep - gofmt -w . (11 files of alignment drift from recent edits) - install.sh / install.ps1: accept $GITHUB_TOKEN so the installers work against the repo while it's private; no-op on public repos - README: document the private-repo install paths (PAT for curl|bash and powershell, GOPRIVATE for go install)
- commit
- 2158c272afd7bbc914c7f5157518d0e7a8c0990d
- author
- patriceckhart · · 2026-04-18 08:55 UTC
- parent
- 4712f5e
- signature
- unsigned
17 files · +159 -109
M README.md +23 -2
| @@ -12,10 +12,20 @@ yet another coding agent harness, lightweight and written (vibe-slopped) in go. | |||
| 12 | 12 | ||
| 13 | 13 | ## install | |
| 14 | 14 | ||
| 15 | + | > the repo and prebuilt artifacts are **private at the moment**. until they go public, every download command below needs a github personal access token (PAT) with `contents:read` scope on `patriceckhart/zot`. create one at <https://github.com/settings/tokens> and export it before running any installer: | |
| 16 | + | > | |
| 17 | + | > ```bash | |
| 18 | + | > export GITHUB_TOKEN=ghp_xxx | |
| 19 | + | > ``` | |
| 20 | + | > | |
| 21 | + | > once the repo is public, the token becomes optional. | |
| 22 | + | ||
| 15 | 23 | ### one-liner (macos + linux) | |
| 16 | 24 | ||
| 17 | 25 | ```bash | |
| 18 | - | curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash | |
| 26 | + | curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
| 27 | + | https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh \ | |
| 28 | + | | GITHUB_TOKEN=$GITHUB_TOKEN bash | |
| 19 | 29 | ``` | |
| 20 | 30 | ||
| 21 | 31 | 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: | |
| @@ -27,23 +37,34 @@ curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | | |||
| 27 | 37 | ### one-liner (windows, powershell) | |
| 28 | 38 | ||
| 29 | 39 | ```powershell | |
| 30 | - | iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex | |
| 40 | + | $env:GITHUB_TOKEN = "ghp_xxx" | |
| 41 | + | $h = @{ Authorization = "Bearer $env:GITHUB_TOKEN" } | |
| 42 | + | (Invoke-WebRequest -UseBasicParsing -Headers $h ` | |
| 43 | + | https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1).Content | iex | |
| 31 | 44 | ``` | |
| 32 | 45 | ||
| 33 | 46 | drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. open a fresh terminal afterwards. | |
| 34 | 47 | ||
| 35 | 48 | ### homebrew (macos + linux) | |
| 36 | 49 | ||
| 50 | + | available once the repo is public. the tap lives at `patriceckhart/homebrew-tap`: | |
| 51 | + | ||
| 37 | 52 | ```bash | |
| 38 | 53 | brew install patriceckhart/tap/zot | |
| 39 | 54 | ``` | |
| 40 | 55 | ||
| 41 | 56 | ### go install | |
| 42 | 57 | ||
| 58 | + | while the repo is private, `go install` needs to bypass the public go module proxy: | |
| 59 | + | ||
| 43 | 60 | ```bash | |
| 61 | + | export GOPRIVATE=github.com/patriceckhart/* | |
| 62 | + | export GIT_TERMINAL_PROMPT=0 | |
| 44 | 63 | go install github.com/patriceckhart/zot/cmd/zot@latest | |
| 45 | 64 | ``` | |
| 46 | 65 | ||
| 66 | + | your git must already be authenticated to github (ssh key or https PAT in `~/.netrc`). once the repo is public, `go install github.com/patriceckhart/zot/cmd/zot@latest` is enough. | |
| 67 | + | ||
| 47 | 68 | ### from source | |
| 48 | 69 | ||
| 49 | 70 | ```bash | |
| @@ -12,10 +12,20 @@ yet another coding agent harness, lightweight and written (vibe-slopped) in go. | |||||
| 12 | 12 | ||||
| 13 | ## install | 13 | ## install | ||
| 14 | 14 | ||||
| 15 | + | > the repo and prebuilt artifacts are **private at the moment**. until they go public, every download command below needs a github personal access token (PAT) with `contents:read` scope on `patriceckhart/zot`. create one at <https://github.com/settings/tokens> and export it before running any installer: | |||
| 16 | + | > | |||
| 17 | + | > ```bash | |||
| 18 | + | > export GITHUB_TOKEN=ghp_xxx | |||
| 19 | + | > ``` | |||
| 20 | + | > | |||
| 21 | + | > once the repo is public, the token becomes optional. | |||
| 22 | + | ||||
| 15 | ### one-liner (macos + linux) | 23 | ### one-liner (macos + linux) | ||
| 16 | 24 | ||||
| 17 | ```bash | 25 | ```bash | ||
| 18 | - | curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | bash | 26 | + | curl -fsSL -H "Authorization: Bearer $GITHUB_TOKEN" \ |
| 27 | + | https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh \ | |||
| 28 | + | | GITHUB_TOKEN=$GITHUB_TOKEN bash | |||
| 19 | ``` | 29 | ``` | ||
| 20 | 30 | ||||
| 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: | 31 | 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: | ||
| @@ -27,23 +37,34 @@ curl -fsSL https://raw.githubusercontent.com/patriceckhart/zot/main/install.sh | | |||||
| 27 | ### one-liner (windows, powershell) | 37 | ### one-liner (windows, powershell) | ||
| 28 | 38 | ||||
| 29 | ```powershell | 39 | ```powershell | ||
| 30 | - | iwr -useb https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1 | iex | 40 | + | $env:GITHUB_TOKEN = "ghp_xxx" |
| 41 | + | $h = @{ Authorization = "Bearer $env:GITHUB_TOKEN" } | |||
| 42 | + | (Invoke-WebRequest -UseBasicParsing -Headers $h ` | |||
| 43 | + | https://raw.githubusercontent.com/patriceckhart/zot/main/install.ps1).Content | iex | |||
| 31 | ``` | 44 | ``` | ||
| 32 | 45 | ||||
| 33 | drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. open a fresh terminal afterwards. | 46 | drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. open a fresh terminal afterwards. | ||
| 34 | 47 | ||||
| 35 | ### homebrew (macos + linux) | 48 | ### homebrew (macos + linux) | ||
| 36 | 49 | ||||
| 50 | + | available once the repo is public. the tap lives at `patriceckhart/homebrew-tap`: | |||
| 51 | + | ||||
| 37 | ```bash | 52 | ```bash | ||
| 38 | brew install patriceckhart/tap/zot | 53 | brew install patriceckhart/tap/zot | ||
| 39 | ``` | 54 | ``` | ||
| 40 | 55 | ||||
| 41 | ### go install | 56 | ### go install | ||
| 42 | 57 | ||||
| 58 | + | while the repo is private, `go install` needs to bypass the public go module proxy: | |||
| 59 | + | ||||
| 43 | ```bash | 60 | ```bash | ||
| 61 | + | export GOPRIVATE=github.com/patriceckhart/* | |||
| 62 | + | export GIT_TERMINAL_PROMPT=0 | |||
| 44 | go install github.com/patriceckhart/zot/cmd/zot@latest | 63 | go install github.com/patriceckhart/zot/cmd/zot@latest | ||
| 45 | ``` | 64 | ``` | ||
| 46 | 65 | ||||
| 66 | + | your git must already be authenticated to github (ssh key or https PAT in `~/.netrc`). once the repo is public, `go install github.com/patriceckhart/zot/cmd/zot@latest` is enough. | |||
| 67 | + | ||||
| 47 | ### from source | 68 | ### from source | ||
| 48 | 69 | ||||
| 49 | ```bash | 70 | ```bash | ||
M go.mod +2 -2
| @@ -3,14 +3,14 @@ module github.com/patriceckhart/zot | |||
| 3 | 3 | go 1.22 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | + | github.com/alecthomas/chroma/v2 v2.23.1 | |
| 6 | 7 | github.com/google/uuid v1.6.0 | |
| 7 | 8 | github.com/mattn/go-runewidth v0.0.16 | |
| 9 | + | golang.org/x/sys v0.26.0 | |
| 8 | 10 | golang.org/x/term v0.25.0 | |
| 9 | 11 | ) | |
| 10 | 12 | ||
| 11 | 13 | require ( | |
| 12 | - | github.com/alecthomas/chroma/v2 v2.23.1 // indirect | |
| 13 | 14 | github.com/dlclark/regexp2 v1.11.5 // indirect | |
| 14 | 15 | github.com/rivo/uniseg v0.2.0 // indirect | |
| 15 | - | golang.org/x/sys v0.26.0 // indirect | |
| 16 | 16 | ) | |
| @@ -3,14 +3,14 @@ module github.com/patriceckhart/zot | |||||
| 3 | go 1.22 | 3 | go 1.22 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | + | github.com/alecthomas/chroma/v2 v2.23.1 | |||
| 6 | github.com/google/uuid v1.6.0 | 7 | github.com/google/uuid v1.6.0 | ||
| 7 | github.com/mattn/go-runewidth v0.0.16 | 8 | github.com/mattn/go-runewidth v0.0.16 | ||
| 9 | + | golang.org/x/sys v0.26.0 | |||
| 8 | golang.org/x/term v0.25.0 | 10 | golang.org/x/term v0.25.0 | ||
| 9 | ) | 11 | ) | ||
| 10 | 12 | ||||
| 11 | require ( | 13 | require ( | ||
| 12 | - | github.com/alecthomas/chroma/v2 v2.23.1 // indirect | |||
| 13 | github.com/dlclark/regexp2 v1.11.5 // indirect | 14 | github.com/dlclark/regexp2 v1.11.5 // indirect | ||
| 14 | github.com/rivo/uniseg v0.2.0 // indirect | 15 | github.com/rivo/uniseg v0.2.0 // indirect | ||
| 15 | - | golang.org/x/sys v0.26.0 // indirect | |||
| 16 | ) | 16 | ) | ||
M go.sum +6 -4 generated file: collapsed
M install.ps1 +22 -7
| @@ -12,6 +12,11 @@ | |||
| 12 | 12 | # release, verifies the sha256 against checksums.txt, extracts zot.exe, | |
| 13 | 13 | # and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH | |
| 14 | 14 | # via the User environment if missing). | |
| 15 | + | # | |
| 16 | + | # While the repo is private, set $env:GITHUB_TOKEN to a PAT with | |
| 17 | + | # `contents:read` scope; the script uses it for every download. Once | |
| 18 | + | # the repo goes public, the token becomes optional. | |
| 19 | + | ||
| 15 | 20 | ||
| 16 | 21 | [CmdletBinding()] | |
| 17 | 22 | param( | |
| @@ -25,6 +30,11 @@ $owner = "patriceckhart" | |||
| 25 | 30 | $repo = "zot" | |
| 26 | 31 | $binary = "zot" | |
| 27 | 32 | ||
| 33 | + | # Build Authorization header list once; used on every HTTP call so the | |
| 34 | + | # script works against private repos when $env:GITHUB_TOKEN is set. | |
| 35 | + | $headers = @{} | |
| 36 | + | if ($env:GITHUB_TOKEN) { $headers["Authorization"] = "Bearer $($env:GITHUB_TOKEN)" } | |
| 37 | + | ||
| 28 | 38 | if (-not $Version) { $Version = "latest" } | |
| 29 | 39 | if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" } | |
| 30 | 40 | ||
| @@ -50,12 +60,17 @@ if ($arch -eq "arm64") { | |||
| 50 | 60 | # ---- resolve version ---- | |
| 51 | 61 | ||
| 52 | 62 | 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] | |
| 63 | + | if ($headers.ContainsKey("Authorization")) { | |
| 64 | + | $api = Invoke-RestMethod -Headers $headers "https://api.github.com/repos/$owner/$repo/releases/latest" | |
| 65 | + | $Version = $api.tag_name | |
| 57 | 66 | } else { | |
| 58 | - | Die "could not resolve latest version" | |
| 67 | + | $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 ` | |
| 68 | + | "https://github.com/$owner/$repo/releases/latest" | |
| 69 | + | if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') { | |
| 70 | + | $Version = $Matches[1] | |
| 71 | + | } else { | |
| 72 | + | Die "could not resolve latest version (set `$env:GITHUB_TOKEN if the repo is private)" | |
| 73 | + | } | |
| 59 | 74 | } | |
| 60 | 75 | } | |
| 61 | 76 | ||
| @@ -73,10 +88,10 @@ $tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("zot-install-" + | |||
| 73 | 88 | ||
| 74 | 89 | try { | |
| 75 | 90 | Msg "downloading $archive" | |
| 76 | - | Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile (Join-Path $tmp $archive) | |
| 91 | + | Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $archiveUrl -OutFile (Join-Path $tmp $archive) | |
| 77 | 92 | ||
| 78 | 93 | Msg "verifying checksum" | |
| 79 | - | $checksums = Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl | Select-Object -ExpandProperty Content | |
| 94 | + | $checksums = Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $checksumUrl | Select-Object -ExpandProperty Content | |
| 80 | 95 | $expected = ($checksums -split "`n" | Where-Object { $_ -match [regex]::Escape($archive) + "$" } | Select-Object -First 1) | |
| 81 | 96 | if (-not $expected) { Die "no checksum for $archive in checksums.txt" } | |
| 82 | 97 | $expectedHash = ($expected -split "\s+")[0] | |
| @@ -12,6 +12,11 @@ | |||||
| 12 | # release, verifies the sha256 against checksums.txt, extracts zot.exe, | 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 | 13 | # and moves it into $ZOT_PREFIX (defaults to $HOME\bin, added to PATH | ||
| 14 | # via the User environment if missing). | 14 | # via the User environment if missing). | ||
| 15 | + | # | |||
| 16 | + | # While the repo is private, set $env:GITHUB_TOKEN to a PAT with | |||
| 17 | + | # `contents:read` scope; the script uses it for every download. Once | |||
| 18 | + | # the repo goes public, the token becomes optional. | |||
| 19 | + | ||||
| 15 | 20 | ||||
| 16 | [CmdletBinding()] | 21 | [CmdletBinding()] | ||
| 17 | param( | 22 | param( | ||
| @@ -25,6 +30,11 @@ $owner = "patriceckhart" | |||||
| 25 | $repo = "zot" | 30 | $repo = "zot" | ||
| 26 | $binary = "zot" | 31 | $binary = "zot" | ||
| 27 | 32 | ||||
| 33 | + | # Build Authorization header list once; used on every HTTP call so the | |||
| 34 | + | # script works against private repos when $env:GITHUB_TOKEN is set. | |||
| 35 | + | $headers = @{} | |||
| 36 | + | if ($env:GITHUB_TOKEN) { $headers["Authorization"] = "Bearer $($env:GITHUB_TOKEN)" } | |||
| 37 | + | ||||
| 28 | if (-not $Version) { $Version = "latest" } | 38 | if (-not $Version) { $Version = "latest" } | ||
| 29 | if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" } | 39 | if (-not $Prefix) { $Prefix = Join-Path $HOME "bin" } | ||
| 30 | 40 | ||||
| @@ -50,12 +60,17 @@ if ($arch -eq "arm64") { | |||||
| 50 | # ---- resolve version ---- | 60 | # ---- resolve version ---- | ||
| 51 | 61 | ||||
| 52 | if ($Version -eq "latest") { | 62 | if ($Version -eq "latest") { | ||
| 53 | - | $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 ` | 63 | + | if ($headers.ContainsKey("Authorization")) { |
| 54 | - | "https://github.com/$owner/$repo/releases/latest" | 64 | + | $api = Invoke-RestMethod -Headers $headers "https://api.github.com/repos/$owner/$repo/releases/latest" |
| 55 | - | if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') { | 65 | + | $Version = $api.tag_name |
| 56 | - | $Version = $Matches[1] | |||
| 57 | } else { | 66 | } else { | ||
| 58 | - | Die "could not resolve latest version" | 67 | + | $resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 5 ` |
| 68 | + | "https://github.com/$owner/$repo/releases/latest" | |||
| 69 | + | if ($resp.BaseResponse.ResponseUri -match '/tag/([^/]+)') { | |||
| 70 | + | $Version = $Matches[1] | |||
| 71 | + | } else { | |||
| 72 | + | Die "could not resolve latest version (set `$env:GITHUB_TOKEN if the repo is private)" | |||
| 73 | + | } | |||
| 59 | } | 74 | } | ||
| 60 | } | 75 | } | ||
| 61 | 76 | ||||
| @@ -73,10 +88,10 @@ $tmp = New-Item -ItemType Directory -Path (Join-Path $env:TEMP ("zot-install-" + | |||||
| 73 | 88 | ||||
| 74 | try { | 89 | try { | ||
| 75 | Msg "downloading $archive" | 90 | Msg "downloading $archive" | ||
| 76 | - | Invoke-WebRequest -UseBasicParsing -Uri $archiveUrl -OutFile (Join-Path $tmp $archive) | 91 | + | Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $archiveUrl -OutFile (Join-Path $tmp $archive) |
| 77 | 92 | ||||
| 78 | Msg "verifying checksum" | 93 | Msg "verifying checksum" | ||
| 79 | - | $checksums = Invoke-WebRequest -UseBasicParsing -Uri $checksumUrl | Select-Object -ExpandProperty Content | 94 | + | $checksums = Invoke-WebRequest -UseBasicParsing -Headers $headers -Uri $checksumUrl | Select-Object -ExpandProperty Content |
| 80 | $expected = ($checksums -split "`n" | Where-Object { $_ -match [regex]::Escape($archive) + "$" } | Select-Object -First 1) | 95 | $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" } | 96 | if (-not $expected) { Die "no checksum for $archive in checksums.txt" } | ||
| 82 | $expectedHash = ($expected -split "\s+")[0] | 97 | $expectedHash = ($expected -split "\s+")[0] | ||
M install.sh +26 -10
| @@ -16,6 +16,9 @@ | |||
| 16 | 16 | # Environment overrides: | |
| 17 | 17 | # ZOT_VERSION same as $1 | |
| 18 | 18 | # ZOT_PREFIX same as $2 | |
| 19 | + | # GITHUB_TOKEN personal access token — required while the repo is | |
| 20 | + | # private, ignored once it goes public. Must have at | |
| 21 | + | # least `contents:read` scope on the zot repository. | |
| 19 | 22 | # | |
| 20 | 23 | # The script detects your OS and architecture, downloads the matching | |
| 21 | 24 | # archive from the GitHub release, verifies the sha256 against the | |
| @@ -39,6 +42,14 @@ die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; } | |||
| 39 | 42 | command -v curl >/dev/null 2>&1 || die "curl is required" | |
| 40 | 43 | command -v tar >/dev/null 2>&1 || die "tar is required" | |
| 41 | 44 | ||
| 45 | + | # CURL_AUTH is prepended to every curl invocation so private-repo | |
| 46 | + | # downloads work while $GITHUB_TOKEN is set. Empty string when the repo | |
| 47 | + | # becomes public. | |
| 48 | + | CURL_AUTH=() | |
| 49 | + | if [ -n "${GITHUB_TOKEN:-}" ]; then | |
| 50 | + | CURL_AUTH=(-H "Authorization: Bearer $GITHUB_TOKEN") | |
| 51 | + | fi | |
| 52 | + | ||
| 42 | 53 | # ---- detect OS + arch ---- | |
| 43 | 54 | ||
| 44 | 55 | uname_s=$(uname -s) | |
| @@ -62,13 +73,18 @@ esac | |||
| 62 | 73 | # ---- resolve version ---- | |
| 63 | 74 | ||
| 64 | 75 | 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" | |
| 76 | + | # Private-repo friendly: hit the api, grab tag_name. Falls back to | |
| 77 | + | # following the /releases/latest redirect on public repos. | |
| 78 | + | if [ ${#CURL_AUTH[@]} -gt 0 ]; then | |
| 79 | + | VERSION=$(curl -fsSL "${CURL_AUTH[@]}" \ | |
| 80 | + | "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" \ | |
| 81 | + | | sed -nE 's/.*"tag_name": *"([^"]+)".*/\1/p' | head -n1) | |
| 82 | + | else | |
| 83 | + | VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \ | |
| 84 | + | "https://github.com/${OWNER}/${REPO}/releases/latest" \ | |
| 85 | + | | sed -E 's|.*/tag/([^/]+).*|\1|') | |
| 86 | + | fi | |
| 87 | + | [ -n "$VERSION" ] || die "could not resolve latest version (set GITHUB_TOKEN if the repo is private)" | |
| 72 | 88 | fi | |
| 73 | 89 | ||
| 74 | 90 | case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac | |
| @@ -110,11 +126,11 @@ TMP=$(mktemp -d) | |||
| 110 | 126 | trap 'rm -rf "$TMP"' EXIT | |
| 111 | 127 | ||
| 112 | 128 | msg "downloading ${ARCHIVE}" | |
| 113 | - | curl -fsSL -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \ | |
| 114 | - | || die "download failed: $ARCHIVE_URL" | |
| 129 | + | curl -fsSL "${CURL_AUTH[@]}" -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \ | |
| 130 | + | || die "download failed: $ARCHIVE_URL (set GITHUB_TOKEN if the repo is private)" | |
| 115 | 131 | ||
| 116 | 132 | msg "verifying checksum" | |
| 117 | - | curl -fsSL -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \ | |
| 133 | + | curl -fsSL "${CURL_AUTH[@]}" -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \ | |
| 118 | 134 | || die "download failed: $CHECKSUMS_URL" | |
| 119 | 135 | ||
| 120 | 136 | expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true) | |
| @@ -16,6 +16,9 @@ | |||||
| 16 | # Environment overrides: | 16 | # Environment overrides: | ||
| 17 | # ZOT_VERSION same as $1 | 17 | # ZOT_VERSION same as $1 | ||
| 18 | # ZOT_PREFIX same as $2 | 18 | # ZOT_PREFIX same as $2 | ||
| 19 | + | # GITHUB_TOKEN personal access token — required while the repo is | |||
| 20 | + | # private, ignored once it goes public. Must have at | |||
| 21 | + | # least `contents:read` scope on the zot repository. | |||
| 19 | # | 22 | # | ||
| 20 | # The script detects your OS and architecture, downloads the matching | 23 | # The script detects your OS and architecture, downloads the matching | ||
| 21 | # archive from the GitHub release, verifies the sha256 against the | 24 | # archive from the GitHub release, verifies the sha256 against the | ||
| @@ -39,6 +42,14 @@ die() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; exit 1; } | |||||
| 39 | command -v curl >/dev/null 2>&1 || die "curl is required" | 42 | command -v curl >/dev/null 2>&1 || die "curl is required" | ||
| 40 | command -v tar >/dev/null 2>&1 || die "tar is required" | 43 | command -v tar >/dev/null 2>&1 || die "tar is required" | ||
| 41 | 44 | ||||
| 45 | + | # CURL_AUTH is prepended to every curl invocation so private-repo | |||
| 46 | + | # downloads work while $GITHUB_TOKEN is set. Empty string when the repo | |||
| 47 | + | # becomes public. | |||
| 48 | + | CURL_AUTH=() | |||
| 49 | + | if [ -n "${GITHUB_TOKEN:-}" ]; then | |||
| 50 | + | CURL_AUTH=(-H "Authorization: Bearer $GITHUB_TOKEN") | |||
| 51 | + | fi | |||
| 52 | + | ||||
| 42 | # ---- detect OS + arch ---- | 53 | # ---- detect OS + arch ---- | ||
| 43 | 54 | ||||
| 44 | uname_s=$(uname -s) | 55 | uname_s=$(uname -s) | ||
| @@ -62,13 +73,18 @@ esac | |||||
| 62 | # ---- resolve version ---- | 73 | # ---- resolve version ---- | ||
| 63 | 74 | ||||
| 64 | if [ "$VERSION" = "latest" ]; then | 75 | if [ "$VERSION" = "latest" ]; then | ||
| 65 | - | # GitHub's /releases/latest endpoint redirects to /releases/tag/<tag>. | 76 | + | # Private-repo friendly: hit the api, grab tag_name. Falls back to |
| 66 | - | # We follow the redirect and grab the tag from the final URL without | 77 | + | # following the /releases/latest redirect on public repos. |
| 67 | - | # needing jq. | 78 | + | if [ ${#CURL_AUTH[@]} -gt 0 ]; then |
| 68 | - | VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \ | 79 | + | VERSION=$(curl -fsSL "${CURL_AUTH[@]}" \ |
| 69 | - | "https://github.com/${OWNER}/${REPO}/releases/latest" \ | 80 | + | "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" \ |
| 70 | - | | sed -E 's|.*/tag/([^/]+).*|\1|') | 81 | + | | sed -nE 's/.*"tag_name": *"([^"]+)".*/\1/p' | head -n1) |
| 71 | - | [ -n "$VERSION" ] || die "could not resolve latest version" | 82 | + | else |
| 83 | + | VERSION=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \ | |||
| 84 | + | "https://github.com/${OWNER}/${REPO}/releases/latest" \ | |||
| 85 | + | | sed -E 's|.*/tag/([^/]+).*|\1|') | |||
| 86 | + | fi | |||
| 87 | + | [ -n "$VERSION" ] || die "could not resolve latest version (set GITHUB_TOKEN if the repo is private)" | |||
| 72 | fi | 88 | fi | ||
| 73 | 89 | ||||
| 74 | case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac | 90 | case "$VERSION" in v*) ;; *) VERSION="v$VERSION" ;; esac | ||
| @@ -110,11 +126,11 @@ TMP=$(mktemp -d) | |||||
| 110 | trap 'rm -rf "$TMP"' EXIT | 126 | trap 'rm -rf "$TMP"' EXIT | ||
| 111 | 127 | ||||
| 112 | msg "downloading ${ARCHIVE}" | 128 | msg "downloading ${ARCHIVE}" | ||
| 113 | - | curl -fsSL -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \ | 129 | + | curl -fsSL "${CURL_AUTH[@]}" -o "$TMP/$ARCHIVE" "$ARCHIVE_URL" \ |
| 114 | - | || die "download failed: $ARCHIVE_URL" | 130 | + | || die "download failed: $ARCHIVE_URL (set GITHUB_TOKEN if the repo is private)" |
| 115 | 131 | ||||
| 116 | msg "verifying checksum" | 132 | msg "verifying checksum" | ||
| 117 | - | curl -fsSL -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \ | 133 | + | curl -fsSL "${CURL_AUTH[@]}" -o "$TMP/checksums.txt" "$CHECKSUMS_URL" \ |
| 118 | || die "download failed: $CHECKSUMS_URL" | 134 | || die "download failed: $CHECKSUMS_URL" | ||
| 119 | 135 | ||||
| 120 | expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true) | 136 | expected=$(grep " ${ARCHIVE}\$" "$TMP/checksums.txt" | awk '{print $1}' || true) | ||
M internal/agent/cli.go +15 -15
| @@ -200,21 +200,21 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 200 | 200 | ||
| 201 | 201 | term := tui.NewProcTerm() | |
| 202 | 202 | iv := modes.NewInteractive(modes.InteractiveConfig{ | |
| 203 | - | Terminal: term, | |
| 204 | - | Theme: tui.Dark, | |
| 205 | - | Model: r.Model, | |
| 206 | - | Provider: r.Provider, | |
| 207 | - | AuthMethod: r.AuthMethod, | |
| 208 | - | BaseURL: r.BaseURL, | |
| 209 | - | Reasoning: r.Reasoning, | |
| 210 | - | SystemPrompt: r.SystemPrompt, | |
| 211 | - | Tools: r.ToolRegistry, | |
| 212 | - | MaxSteps: r.MaxSteps, | |
| 213 | - | CWD: r.CWD, | |
| 214 | - | ZotHome: ZotHome(), | |
| 215 | - | Sandbox: sharedSandbox, | |
| 216 | - | Agent: ag, | |
| 217 | - | InitialInput: args.Prompt, | |
| 203 | + | Terminal: term, | |
| 204 | + | Theme: tui.Dark, | |
| 205 | + | Model: r.Model, | |
| 206 | + | Provider: r.Provider, | |
| 207 | + | AuthMethod: r.AuthMethod, | |
| 208 | + | BaseURL: r.BaseURL, | |
| 209 | + | Reasoning: r.Reasoning, | |
| 210 | + | SystemPrompt: r.SystemPrompt, | |
| 211 | + | Tools: r.ToolRegistry, | |
| 212 | + | MaxSteps: r.MaxSteps, | |
| 213 | + | CWD: r.CWD, | |
| 214 | + | ZotHome: ZotHome(), | |
| 215 | + | Sandbox: sharedSandbox, | |
| 216 | + | Agent: ag, | |
| 217 | + | InitialInput: args.Prompt, | |
| 218 | 218 | AuthManager: mgr, | |
| 219 | 219 | BuildAgent: buildAgent, | |
| 220 | 220 | BuildAgentFor: buildAgentFor, | |
| @@ -200,21 +200,21 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 200 | 200 | ||||
| 201 | term := tui.NewProcTerm() | 201 | term := tui.NewProcTerm() | ||
| 202 | iv := modes.NewInteractive(modes.InteractiveConfig{ | 202 | iv := modes.NewInteractive(modes.InteractiveConfig{ | ||
| 203 | - | Terminal: term, | 203 | + | Terminal: term, |
| 204 | - | Theme: tui.Dark, | 204 | + | Theme: tui.Dark, |
| 205 | - | Model: r.Model, | 205 | + | Model: r.Model, |
| 206 | - | Provider: r.Provider, | 206 | + | Provider: r.Provider, |
| 207 | - | AuthMethod: r.AuthMethod, | 207 | + | AuthMethod: r.AuthMethod, |
| 208 | - | BaseURL: r.BaseURL, | 208 | + | BaseURL: r.BaseURL, |
| 209 | - | Reasoning: r.Reasoning, | 209 | + | Reasoning: r.Reasoning, |
| 210 | - | SystemPrompt: r.SystemPrompt, | 210 | + | SystemPrompt: r.SystemPrompt, |
| 211 | - | Tools: r.ToolRegistry, | 211 | + | Tools: r.ToolRegistry, |
| 212 | - | MaxSteps: r.MaxSteps, | 212 | + | MaxSteps: r.MaxSteps, |
| 213 | - | CWD: r.CWD, | 213 | + | CWD: r.CWD, |
| 214 | - | ZotHome: ZotHome(), | 214 | + | ZotHome: ZotHome(), |
| 215 | - | Sandbox: sharedSandbox, | 215 | + | Sandbox: sharedSandbox, |
| 216 | - | Agent: ag, | 216 | + | Agent: ag, |
| 217 | - | InitialInput: args.Prompt, | 217 | + | InitialInput: args.Prompt, |
| 218 | AuthManager: mgr, | 218 | AuthManager: mgr, | ||
| 219 | BuildAgent: buildAgent, | 219 | BuildAgent: buildAgent, | ||
| 220 | BuildAgentFor: buildAgentFor, | 220 | BuildAgentFor: buildAgentFor, | ||
M internal/agent/modes/dialog_frame.go +1 -1
| @@ -10,7 +10,7 @@ import ( | |||
| 10 | 10 | // Matches the thin rule style used for code blocks and tool results so | |
| 11 | 11 | // every dialog in the TUI looks the same. | |
| 12 | 12 | // | |
| 13 | - | // ─── title ──────────────────────────────── | |
| 13 | + | // ─── title ──────────────────────────────── | |
| 14 | 14 | func frameHeader(th tui.Theme, title string, width int) string { | |
| 15 | 15 | label := "── " + title + " " | |
| 16 | 16 | if width <= 0 { | |
| @@ -10,7 +10,7 @@ import ( | |||||
| 10 | // Matches the thin rule style used for code blocks and tool results so | 10 | // Matches the thin rule style used for code blocks and tool results so | ||
| 11 | // every dialog in the TUI looks the same. | 11 | // every dialog in the TUI looks the same. | ||
| 12 | // | 12 | // | ||
| 13 | - | // ─── title ──────────────────────────────── | 13 | + | // ─── title ──────────────────────────────── |
| 14 | func frameHeader(th tui.Theme, title string, width int) string { | 14 | func frameHeader(th tui.Theme, title string, width int) string { | ||
| 15 | label := "── " + title + " " | 15 | label := "── " + title + " " | ||
| 16 | if width <= 0 { | 16 | if width <= 0 { | ||
M internal/agent/modes/interactive.go +4 -4
| @@ -120,10 +120,10 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||
| 120 | 120 | Theme: cfg.Theme, | |
| 121 | 121 | ImageProto: tui.DetectImageProtocol(), | |
| 122 | 122 | }, | |
| 123 | - | ed: tui.NewEditor(cfg.Theme.FG256(cfg.Theme.Accent, "▌ ")), | |
| 124 | - | rend: tui.NewRenderer(cfg.Terminal), | |
| 125 | - | toolCalls: map[string]*tui.ToolCallView{}, | |
| 126 | - | dirty: make(chan struct{}, 8), | |
| 123 | + | ed: tui.NewEditor(cfg.Theme.FG256(cfg.Theme.Accent, "▌ ")), | |
| 124 | + | rend: tui.NewRenderer(cfg.Terminal), | |
| 125 | + | toolCalls: map[string]*tui.ToolCallView{}, | |
| 126 | + | dirty: make(chan struct{}, 8), | |
| 127 | 127 | dialog: newLoginDialog(), | |
| 128 | 128 | modelDialog: newModelDialog(), | |
| 129 | 129 | sessionDialog: newSessionDialog(), | |
| @@ -120,10 +120,10 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||||
| 120 | Theme: cfg.Theme, | 120 | Theme: cfg.Theme, | ||
| 121 | ImageProto: tui.DetectImageProtocol(), | 121 | ImageProto: tui.DetectImageProtocol(), | ||
| 122 | }, | 122 | }, | ||
| 123 | - | ed: tui.NewEditor(cfg.Theme.FG256(cfg.Theme.Accent, "▌ ")), | 123 | + | ed: tui.NewEditor(cfg.Theme.FG256(cfg.Theme.Accent, "▌ ")), |
| 124 | - | rend: tui.NewRenderer(cfg.Terminal), | 124 | + | rend: tui.NewRenderer(cfg.Terminal), |
| 125 | - | toolCalls: map[string]*tui.ToolCallView{}, | 125 | + | toolCalls: map[string]*tui.ToolCallView{}, |
| 126 | - | dirty: make(chan struct{}, 8), | 126 | + | dirty: make(chan struct{}, 8), |
| 127 | dialog: newLoginDialog(), | 127 | dialog: newLoginDialog(), | ||
| 128 | modelDialog: newModelDialog(), | 128 | modelDialog: newModelDialog(), | ||
| 129 | sessionDialog: newSessionDialog(), | 129 | sessionDialog: newSessionDialog(), | ||
M internal/agent/modes/session_dialog.go +3 -3
| @@ -11,9 +11,9 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | // sessionDialog is the inline picker shown when the user runs /sessions. | |
| 13 | 13 | type sessionDialog struct { | |
| 14 | - | active bool | |
| 15 | - | sessions []core.SessionSummary | |
| 16 | - | cursor int | |
| 14 | + | active bool | |
| 15 | + | sessions []core.SessionSummary | |
| 16 | + | cursor int | |
| 17 | 17 | } | |
| 18 | 18 | ||
| 19 | 19 | // sessionDialogAction is returned by HandleKey. | |
| @@ -11,9 +11,9 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | // sessionDialog is the inline picker shown when the user runs /sessions. | 12 | // sessionDialog is the inline picker shown when the user runs /sessions. | ||
| 13 | type sessionDialog struct { | 13 | type sessionDialog struct { | ||
| 14 | - | active bool | 14 | + | active bool |
| 15 | - | sessions []core.SessionSummary | 15 | + | sessions []core.SessionSummary |
| 16 | - | cursor int | 16 | + | cursor int |
| 17 | } | 17 | } | ||
| 18 | 18 | ||||
| 19 | // sessionDialogAction is returned by HandleKey. | 19 | // sessionDialogAction is returned by HandleKey. | ||
M internal/agent/modes/spinner.go +4 -4
| @@ -9,11 +9,11 @@ import ( | |||
| 9 | 9 | // turn is streaming. It rotates through a list of playful status | |
| 10 | 10 | // messages and a small frame animation. | |
| 11 | 11 | type spinner struct { | |
| 12 | - | frames []string | |
| 13 | - | messages []string | |
| 12 | + | frames []string | |
| 13 | + | messages []string | |
| 14 | 14 | startedAt time.Time | |
| 15 | - | msgIdx int | |
| 16 | - | lastSwap time.Time | |
| 15 | + | msgIdx int | |
| 16 | + | lastSwap time.Time | |
| 17 | 17 | ||
| 18 | 18 | // fixedMsg overrides the rotating funnyWorkingLines message when | |
| 19 | 19 | // set. Used for auto-compaction so the spinner clearly says what's | |
| @@ -9,11 +9,11 @@ import ( | |||||
| 9 | // turn is streaming. It rotates through a list of playful status | 9 | // turn is streaming. It rotates through a list of playful status | ||
| 10 | // messages and a small frame animation. | 10 | // messages and a small frame animation. | ||
| 11 | type spinner struct { | 11 | type spinner struct { | ||
| 12 | - | frames []string | 12 | + | frames []string |
| 13 | - | messages []string | 13 | + | messages []string |
| 14 | startedAt time.Time | 14 | startedAt time.Time | ||
| 15 | - | msgIdx int | 15 | + | msgIdx int |
| 16 | - | lastSwap time.Time | 16 | + | lastSwap time.Time |
| 17 | 17 | ||||
| 18 | // fixedMsg overrides the rotating funnyWorkingLines message when | 18 | // fixedMsg overrides the rotating funnyWorkingLines message when | ||
| 19 | // set. Used for auto-compaction so the spinner clearly says what's | 19 | // set. Used for auto-compaction so the spinner clearly says what's | ||
M internal/provider/openai.go +1 -1
| @@ -308,7 +308,7 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||
| 308 | 308 | } | |
| 309 | 309 | var ( | |
| 310 | 310 | blocks []*blockEntry | |
| 311 | - | currentText *blockEntry // most-recent text block, nil if none | |
| 311 | + | currentText *blockEntry // most-recent text block, nil if none | |
| 312 | 312 | toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block | |
| 313 | 313 | usage Usage | |
| 314 | 314 | stop StopReason = StopEnd | |
| @@ -308,7 +308,7 @@ func (c *openaiClient) runStream(ctx context.Context, resp *http.Response, req R | |||||
| 308 | } | 308 | } | ||
| 309 | var ( | 309 | var ( | ||
| 310 | blocks []*blockEntry | 310 | blocks []*blockEntry | ||
| 311 | - | currentText *blockEntry // most-recent text block, nil if none | 311 | + | currentText *blockEntry // most-recent text block, nil if none |
| 312 | toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block | 312 | toolByIdx = map[int]*blockEntry{} // openai tool_call index -> block | ||
| 313 | usage Usage | 313 | usage Usage | ||
| 314 | stop StopReason = StopEnd | 314 | stop StopReason = StopEnd | ||
M internal/provider/openai_codex.go +14 -14
| @@ -114,15 +114,15 @@ type codexTool struct { | |||
| 114 | 114 | } | |
| 115 | 115 | ||
| 116 | 116 | type codexRequest struct { | |
| 117 | - | Model string `json:"model"` | |
| 118 | - | Store bool `json:"store"` | |
| 119 | - | Stream bool `json:"stream"` | |
| 120 | - | Instructions string `json:"instructions,omitempty"` | |
| 121 | - | Input []any `json:"input"` | |
| 122 | - | Tools []codexTool `json:"tools,omitempty"` | |
| 123 | - | ToolChoice string `json:"tool_choice,omitempty"` | |
| 124 | - | ParallelToolCalls bool `json:"parallel_tool_calls"` | |
| 125 | - | Include []string `json:"include,omitempty"` | |
| 117 | + | Model string `json:"model"` | |
| 118 | + | Store bool `json:"store"` | |
| 119 | + | Stream bool `json:"stream"` | |
| 120 | + | Instructions string `json:"instructions,omitempty"` | |
| 121 | + | Input []any `json:"input"` | |
| 122 | + | Tools []codexTool `json:"tools,omitempty"` | |
| 123 | + | ToolChoice string `json:"tool_choice,omitempty"` | |
| 124 | + | ParallelToolCalls bool `json:"parallel_tool_calls"` | |
| 125 | + | Include []string `json:"include,omitempty"` | |
| 126 | 126 | } | |
| 127 | 127 | ||
| 128 | 128 | // ---- Request building ---- | |
| @@ -305,11 +305,11 @@ func (c *codexClient) runStream(ctx context.Context, resp *http.Response, req Re | |||
| 305 | 305 | announced bool | |
| 306 | 306 | } | |
| 307 | 307 | var ( | |
| 308 | - | items = map[int]*itemState{} | |
| 309 | - | order []int | |
| 310 | - | usage Usage | |
| 311 | - | stop StopReason = StopEnd | |
| 312 | - | finalErr error | |
| 308 | + | items = map[int]*itemState{} | |
| 309 | + | order []int | |
| 310 | + | usage Usage | |
| 311 | + | stop StopReason = StopEnd | |
| 312 | + | finalErr error | |
| 313 | 313 | ) | |
| 314 | 314 | ||
| 315 | 315 | assemble := func() Message { | |
| @@ -114,15 +114,15 @@ type codexTool struct { | |||||
| 114 | } | 114 | } | ||
| 115 | 115 | ||||
| 116 | type codexRequest struct { | 116 | type codexRequest struct { | ||
| 117 | - | Model string `json:"model"` | 117 | + | Model string `json:"model"` |
| 118 | - | Store bool `json:"store"` | 118 | + | Store bool `json:"store"` |
| 119 | - | Stream bool `json:"stream"` | 119 | + | Stream bool `json:"stream"` |
| 120 | - | Instructions string `json:"instructions,omitempty"` | 120 | + | Instructions string `json:"instructions,omitempty"` |
| 121 | - | Input []any `json:"input"` | 121 | + | Input []any `json:"input"` |
| 122 | - | Tools []codexTool `json:"tools,omitempty"` | 122 | + | Tools []codexTool `json:"tools,omitempty"` |
| 123 | - | ToolChoice string `json:"tool_choice,omitempty"` | 123 | + | ToolChoice string `json:"tool_choice,omitempty"` |
| 124 | - | ParallelToolCalls bool `json:"parallel_tool_calls"` | 124 | + | ParallelToolCalls bool `json:"parallel_tool_calls"` |
| 125 | - | Include []string `json:"include,omitempty"` | 125 | + | Include []string `json:"include,omitempty"` |
| 126 | } | 126 | } | ||
| 127 | 127 | ||||
| 128 | // ---- Request building ---- | 128 | // ---- Request building ---- | ||
| @@ -305,11 +305,11 @@ func (c *codexClient) runStream(ctx context.Context, resp *http.Response, req Re | |||||
| 305 | announced bool | 305 | announced bool | ||
| 306 | } | 306 | } | ||
| 307 | var ( | 307 | var ( | ||
| 308 | - | items = map[int]*itemState{} | 308 | + | items = map[int]*itemState{} |
| 309 | - | order []int | 309 | + | order []int |
| 310 | - | usage Usage | 310 | + | usage Usage |
| 311 | - | stop StopReason = StopEnd | 311 | + | stop StopReason = StopEnd |
| 312 | - | finalErr error | 312 | + | finalErr error |
| 313 | ) | 313 | ) | ||
| 314 | 314 | ||||
| 315 | assemble := func() Message { | 315 | assemble := func() Message { | ||
M internal/tui/highlight.go +3 -3
| @@ -58,9 +58,9 @@ func HighlightCode(src, lang string) []string { | |||
| 58 | 58 | // order). That's good enough since tool_result text doesn't change once | |
| 59 | 59 | // emitted, so cache hits are frequent and evictions rare. | |
| 60 | 60 | type highlightResultCache struct { | |
| 61 | - | mu sync.Mutex | |
| 62 | - | max int | |
| 63 | - | data map[string][]string | |
| 61 | + | mu sync.Mutex | |
| 62 | + | max int | |
| 63 | + | data map[string][]string | |
| 64 | 64 | order []string | |
| 65 | 65 | } | |
| 66 | 66 | ||
| @@ -58,9 +58,9 @@ func HighlightCode(src, lang string) []string { | |||||
| 58 | // order). That's good enough since tool_result text doesn't change once | 58 | // order). That's good enough since tool_result text doesn't change once | ||
| 59 | // emitted, so cache hits are frequent and evictions rare. | 59 | // emitted, so cache hits are frequent and evictions rare. | ||
| 60 | type highlightResultCache struct { | 60 | type highlightResultCache struct { | ||
| 61 | - | mu sync.Mutex | 61 | + | mu sync.Mutex |
| 62 | - | max int | 62 | + | max int |
| 63 | - | data map[string][]string | 63 | + | data map[string][]string |
| 64 | order []string | 64 | order []string | ||
| 65 | } | 65 | } | ||
| 66 | 66 | ||||
M internal/tui/image.go +5 -4
| @@ -28,10 +28,11 @@ const ( | |||
| 28 | 28 | // The default is to auto-detect: if the terminal advertises iTerm2 or | |
| 29 | 29 | // Kitty-graphics support, we use it. The ZOT_INLINE_IMAGES env var | |
| 30 | 30 | // overrides the default: | |
| 31 | - | // ZOT_INLINE_IMAGES=off -> force text fallback | |
| 32 | - | // ZOT_INLINE_IMAGES=iterm -> force iTerm2 protocol | |
| 33 | - | // ZOT_INLINE_IMAGES=kitty -> force Kitty protocol | |
| 34 | - | // ZOT_INLINE_IMAGES=auto -> explicit auto-detect (same as default) | |
| 31 | + | // | |
| 32 | + | // ZOT_INLINE_IMAGES=off -> force text fallback | |
| 33 | + | // ZOT_INLINE_IMAGES=iterm -> force iTerm2 protocol | |
| 34 | + | // ZOT_INLINE_IMAGES=kitty -> force Kitty protocol | |
| 35 | + | // ZOT_INLINE_IMAGES=auto -> explicit auto-detect (same as default) | |
| 35 | 36 | func DetectImageProtocol() ImageProtocol { | |
| 36 | 37 | switch strings.ToLower(os.Getenv("ZOT_INLINE_IMAGES")) { | |
| 37 | 38 | case "off", "none", "false", "0": | |
| @@ -28,10 +28,11 @@ const ( | |||||
| 28 | // The default is to auto-detect: if the terminal advertises iTerm2 or | 28 | // The default is to auto-detect: if the terminal advertises iTerm2 or | ||
| 29 | // Kitty-graphics support, we use it. The ZOT_INLINE_IMAGES env var | 29 | // Kitty-graphics support, we use it. The ZOT_INLINE_IMAGES env var | ||
| 30 | // overrides the default: | 30 | // overrides the default: | ||
| 31 | - | // ZOT_INLINE_IMAGES=off -> force text fallback | 31 | + | // |
| 32 | - | // ZOT_INLINE_IMAGES=iterm -> force iTerm2 protocol | 32 | + | // ZOT_INLINE_IMAGES=off -> force text fallback |
| 33 | - | // ZOT_INLINE_IMAGES=kitty -> force Kitty protocol | 33 | + | // ZOT_INLINE_IMAGES=iterm -> force iTerm2 protocol |
| 34 | - | // ZOT_INLINE_IMAGES=auto -> explicit auto-detect (same as default) | 34 | + | // ZOT_INLINE_IMAGES=kitty -> force Kitty protocol |
| 35 | + | // ZOT_INLINE_IMAGES=auto -> explicit auto-detect (same as default) | |||
| 35 | func DetectImageProtocol() ImageProtocol { | 36 | func DetectImageProtocol() ImageProtocol { | ||
| 36 | switch strings.ToLower(os.Getenv("ZOT_INLINE_IMAGES")) { | 37 | switch strings.ToLower(os.Getenv("ZOT_INLINE_IMAGES")) { | ||
| 37 | case "off", "none", "false", "0": | 38 | case "off", "none", "false", "0": | ||
M internal/tui/resize_unix.go +13 -19
| @@ -7,6 +7,8 @@ import ( | |||
| 7 | 7 | "os/signal" | |
| 8 | 8 | "syscall" | |
| 9 | 9 | "time" | |
| 10 | + | ||
| 11 | + | "golang.org/x/sys/unix" | |
| 10 | 12 | ) | |
| 11 | 13 | ||
| 12 | 14 | func (p *ProcTerm) installResizeHandler() { | |
| @@ -27,22 +29,24 @@ func (p *ProcTerm) SetNonblock(enable bool) error { | |||
| 27 | 29 | ||
| 28 | 30 | // peekStdin polls the stdin fd for up to d; if a byte is ready, reads | |
| 29 | 31 | // and returns it. Returns (0, false, nil) on timeout. | |
| 32 | + | // | |
| 33 | + | // Uses golang.org/x/sys/unix so the syscall signatures line up on both | |
| 34 | + | // linux (syscall.Select returns (int, error), Timeval.Usec is int64) | |
| 35 | + | // and darwin (Select returns error only, Timeval.Usec is int32) — | |
| 36 | + | // pulling this through the x/sys wrappers makes both builds happy. | |
| 30 | 37 | func peekStdin(in *os.File, d time.Duration) (byte, bool, error) { | |
| 31 | 38 | fd := int(in.Fd()) | |
| 32 | - | var rset syscall.FdSet | |
| 33 | - | fdSet(&rset, fd) | |
| 34 | - | tv := syscall.Timeval{ | |
| 35 | - | Sec: int64(d / time.Second), | |
| 36 | - | Usec: int32((d % time.Second) / time.Microsecond), | |
| 37 | - | } | |
| 38 | - | err := syscall.Select(fd+1, &rset, nil, nil, &tv) | |
| 39 | + | var rset unix.FdSet | |
| 40 | + | rset.Set(fd) | |
| 41 | + | tv := unix.NsecToTimeval(int64(d)) | |
| 42 | + | _, err := unix.Select(fd+1, &rset, nil, nil, &tv) | |
| 39 | 43 | if err != nil { | |
| 40 | - | if err == syscall.EINTR { | |
| 44 | + | if err == unix.EINTR { | |
| 41 | 45 | return 0, false, nil | |
| 42 | 46 | } | |
| 43 | 47 | return 0, false, err | |
| 44 | 48 | } | |
| 45 | - | if !fdIsSet(&rset, fd) { | |
| 49 | + | if !rset.IsSet(fd) { | |
| 46 | 50 | return 0, false, nil | |
| 47 | 51 | } | |
| 48 | 52 | var b [1]byte | |
| @@ -52,13 +56,3 @@ func peekStdin(in *os.File, d time.Duration) (byte, bool, error) { | |||
| 52 | 56 | } | |
| 53 | 57 | return b[0], true, nil | |
| 54 | 58 | } | |
| 55 | - | ||
| 56 | - | // fdSet sets bit fd in the fd_set. Plain Go implementation so we don't | |
| 57 | - | // need platform-specific helpers from golang.org/x/sys. | |
| 58 | - | func fdSet(set *syscall.FdSet, fd int) { | |
| 59 | - | set.Bits[fd/64] |= 1 << (uint(fd) % 64) | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | func fdIsSet(set *syscall.FdSet, fd int) bool { | |
| 63 | - | return set.Bits[fd/64]&(1<<(uint(fd)%64)) != 0 | |
| 64 | - | } | |
| @@ -7,6 +7,8 @@ import ( | |||||
| 7 | "os/signal" | 7 | "os/signal" | ||
| 8 | "syscall" | 8 | "syscall" | ||
| 9 | "time" | 9 | "time" | ||
| 10 | + | ||||
| 11 | + | "golang.org/x/sys/unix" | |||
| 10 | ) | 12 | ) | ||
| 11 | 13 | ||||
| 12 | func (p *ProcTerm) installResizeHandler() { | 14 | func (p *ProcTerm) installResizeHandler() { | ||
| @@ -27,22 +29,24 @@ func (p *ProcTerm) SetNonblock(enable bool) error { | |||||
| 27 | 29 | ||||
| 28 | // peekStdin polls the stdin fd for up to d; if a byte is ready, reads | 30 | // peekStdin polls the stdin fd for up to d; if a byte is ready, reads | ||
| 29 | // and returns it. Returns (0, false, nil) on timeout. | 31 | // and returns it. Returns (0, false, nil) on timeout. | ||
| 32 | + | // | |||
| 33 | + | // Uses golang.org/x/sys/unix so the syscall signatures line up on both | |||
| 34 | + | // linux (syscall.Select returns (int, error), Timeval.Usec is int64) | |||
| 35 | + | // and darwin (Select returns error only, Timeval.Usec is int32) — | |||
| 36 | + | // pulling this through the x/sys wrappers makes both builds happy. | |||
| 30 | func peekStdin(in *os.File, d time.Duration) (byte, bool, error) { | 37 | func peekStdin(in *os.File, d time.Duration) (byte, bool, error) { | ||
| 31 | fd := int(in.Fd()) | 38 | fd := int(in.Fd()) | ||
| 32 | - | var rset syscall.FdSet | 39 | + | var rset unix.FdSet |
| 33 | - | fdSet(&rset, fd) | 40 | + | rset.Set(fd) |
| 34 | - | tv := syscall.Timeval{ | 41 | + | tv := unix.NsecToTimeval(int64(d)) |
| 35 | - | Sec: int64(d / time.Second), | 42 | + | _, err := unix.Select(fd+1, &rset, nil, nil, &tv) |
| 36 | - | Usec: int32((d % time.Second) / time.Microsecond), | |||
| 37 | - | } | |||
| 38 | - | err := syscall.Select(fd+1, &rset, nil, nil, &tv) | |||
| 39 | if err != nil { | 43 | if err != nil { | ||
| 40 | - | if err == syscall.EINTR { | 44 | + | if err == unix.EINTR { |
| 41 | return 0, false, nil | 45 | return 0, false, nil | ||
| 42 | } | 46 | } | ||
| 43 | return 0, false, err | 47 | return 0, false, err | ||
| 44 | } | 48 | } | ||
| 45 | - | if !fdIsSet(&rset, fd) { | 49 | + | if !rset.IsSet(fd) { |
| 46 | return 0, false, nil | 50 | return 0, false, nil | ||
| 47 | } | 51 | } | ||
| 48 | var b [1]byte | 52 | var b [1]byte | ||
| @@ -52,13 +56,3 @@ func peekStdin(in *os.File, d time.Duration) (byte, bool, error) { | |||||
| 52 | } | 56 | } | ||
| 53 | return b[0], true, nil | 57 | return b[0], true, nil | ||
| 54 | } | 58 | } | ||
| 55 | - | ||||
| 56 | - | // fdSet sets bit fd in the fd_set. Plain Go implementation so we don't | |||
| 57 | - | // need platform-specific helpers from golang.org/x/sys. | |||
| 58 | - | func fdSet(set *syscall.FdSet, fd int) { | |||
| 59 | - | set.Bits[fd/64] |= 1 << (uint(fd) % 64) | |||
| 60 | - | } | |||
| 61 | - | ||||
| 62 | - | func fdIsSet(set *syscall.FdSet, fd int) bool { | |||
| 63 | - | return set.Bits[fd/64]&(1<<(uint(fd)%64)) != 0 | |||
| 64 | - | } | |||
M internal/tui/theme.go +13 -13
| @@ -8,18 +8,18 @@ import "strings" | |||
| 8 | 8 | // ANSI 256-color palette used by zot. Defined as numeric codes so we | |
| 9 | 9 | // can swap themes without changing any render code. | |
| 10 | 10 | type Theme struct { | |
| 11 | - | FG int | |
| 12 | - | Muted int | |
| 13 | - | Accent int | |
| 14 | - | User int // label color for the user role | |
| 15 | - | Assistant int // label color for the zot role | |
| 16 | - | Tool int | |
| 17 | - | ToolOut int | |
| 18 | - | Error int | |
| 19 | - | Warning int | |
| 20 | - | Spinner int // spinner + funny working line | |
| 21 | - | SelectionBG int // background for highlighted rows | |
| 22 | - | SelectionFG int // foreground for highlighted rows | |
| 11 | + | FG int | |
| 12 | + | Muted int | |
| 13 | + | Accent int | |
| 14 | + | User int // label color for the user role | |
| 15 | + | Assistant int // label color for the zot role | |
| 16 | + | Tool int | |
| 17 | + | ToolOut int | |
| 18 | + | Error int | |
| 19 | + | Warning int | |
| 20 | + | Spinner int // spinner + funny working line | |
| 21 | + | SelectionBG int // background for highlighted rows | |
| 22 | + | SelectionFG int // foreground for highlighted rows | |
| 23 | 23 | } | |
| 24 | 24 | ||
| 25 | 25 | var Dark = Theme{ | |
| @@ -47,7 +47,7 @@ var Light = Theme{ | |||
| 47 | 47 | ToolOut: 240, | |
| 48 | 48 | Error: 160, | |
| 49 | 49 | Warning: 166, | |
| 50 | - | Spinner: 91, // purple | |
| 50 | + | Spinner: 91, // purple | |
| 51 | 51 | SelectionBG: 153, // light blue | |
| 52 | 52 | SelectionFG: 232, // near-black | |
| 53 | 53 | } | |
| @@ -8,18 +8,18 @@ import "strings" | |||||
| 8 | // ANSI 256-color palette used by zot. Defined as numeric codes so we | 8 | // ANSI 256-color palette used by zot. Defined as numeric codes so we | ||
| 9 | // can swap themes without changing any render code. | 9 | // can swap themes without changing any render code. | ||
| 10 | type Theme struct { | 10 | type Theme struct { | ||
| 11 | - | FG int | 11 | + | FG int |
| 12 | - | Muted int | 12 | + | Muted int |
| 13 | - | Accent int | 13 | + | Accent int |
| 14 | - | User int // label color for the user role | 14 | + | User int // label color for the user role |
| 15 | - | Assistant int // label color for the zot role | 15 | + | Assistant int // label color for the zot role |
| 16 | - | Tool int | 16 | + | Tool int |
| 17 | - | ToolOut int | 17 | + | ToolOut int |
| 18 | - | Error int | 18 | + | Error int |
| 19 | - | Warning int | 19 | + | Warning int |
| 20 | - | Spinner int // spinner + funny working line | 20 | + | Spinner int // spinner + funny working line |
| 21 | - | SelectionBG int // background for highlighted rows | 21 | + | SelectionBG int // background for highlighted rows |
| 22 | - | SelectionFG int // foreground for highlighted rows | 22 | + | SelectionFG int // foreground for highlighted rows |
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | var Dark = Theme{ | 25 | var Dark = Theme{ | ||
| @@ -47,7 +47,7 @@ var Light = Theme{ | |||||
| 47 | ToolOut: 240, | 47 | ToolOut: 240, | ||
| 48 | Error: 160, | 48 | Error: 160, | ||
| 49 | Warning: 166, | 49 | Warning: 166, | ||
| 50 | - | Spinner: 91, // purple | 50 | + | Spinner: 91, // purple |
| 51 | SelectionBG: 153, // light blue | 51 | SelectionBG: 153, // light blue | ||
| 52 | SelectionFG: 232, // near-black | 52 | SelectionFG: 232, // near-black | ||
| 53 | } | 53 | } | ||
M internal/tui/view.go +4 -3
| @@ -43,7 +43,7 @@ type View struct { | |||
| 43 | 43 | // toolPaths maps tool_use_id to the "path" argument of the call, if | |
| 44 | 44 | // any, so tool_result rendering can pick the right syntax language. | |
| 45 | 45 | // Rebuilt on each Build(). | |
| 46 | - | toolPaths map[string]string | |
| 46 | + | toolPaths map[string]string | |
| 47 | 47 | Streaming string // current assistant text delta | |
| 48 | 48 | StreamingActive bool | |
| 49 | 49 | ToolCalls []ToolCallView // tool calls in flight or completed | |
| @@ -582,8 +582,9 @@ func truncateLines(s string, n int) string { | |||
| 582 | 582 | // StatusBar builds the single-line status shown above the editor. | |
| 583 | 583 | // | |
| 584 | 584 | // Layout: | |
| 585 | - | // [busyPrefix] provider-model tokens-cost ctrl+c hint cwd | |
| 586 | - | // ↑ right-aligned | |
| 585 | + | // | |
| 586 | + | // [busyPrefix] provider-model tokens-cost ctrl+c hint cwd | |
| 587 | + | // ↑ right-aligned | |
| 587 | 588 | // | |
| 588 | 589 | // cols is the terminal width; when > 0 the cwd is placed flush-right | |
| 589 | 590 | // with spaces. busyPrefix, if non-empty, is injected at the far left. | |
| @@ -43,7 +43,7 @@ type View struct { | |||||
| 43 | // toolPaths maps tool_use_id to the "path" argument of the call, if | 43 | // toolPaths maps tool_use_id to the "path" argument of the call, if | ||
| 44 | // any, so tool_result rendering can pick the right syntax language. | 44 | // any, so tool_result rendering can pick the right syntax language. | ||
| 45 | // Rebuilt on each Build(). | 45 | // Rebuilt on each Build(). | ||
| 46 | - | toolPaths map[string]string | 46 | + | toolPaths map[string]string |
| 47 | Streaming string // current assistant text delta | 47 | Streaming string // current assistant text delta | ||
| 48 | StreamingActive bool | 48 | StreamingActive bool | ||
| 49 | ToolCalls []ToolCallView // tool calls in flight or completed | 49 | ToolCalls []ToolCallView // tool calls in flight or completed | ||
| @@ -582,8 +582,9 @@ func truncateLines(s string, n int) string { | |||||
| 582 | // StatusBar builds the single-line status shown above the editor. | 582 | // StatusBar builds the single-line status shown above the editor. | ||
| 583 | // | 583 | // | ||
| 584 | // Layout: | 584 | // Layout: | ||
| 585 | - | // [busyPrefix] provider-model tokens-cost ctrl+c hint cwd | 585 | + | // |
| 586 | - | // ↑ right-aligned | 586 | + | // [busyPrefix] provider-model tokens-cost ctrl+c hint cwd |
| 587 | + | // ↑ right-aligned | |||
| 587 | // | 588 | // | ||
| 588 | // cols is the terminal width; when > 0 the cwd is placed flush-right | 589 | // cols is the terminal width; when > 0 the cwd is placed flush-right | ||
| 589 | // with spaces. busyPrefix, if non-empty, is injected at the far left. | 590 | // with spaces. busyPrefix, if non-empty, is injected at the far left. | ||