Repository: crhuber/kelp
Branch: master
Commit: abde55811893
Files: 24
Total size: 164.1 KB
Directory structure:
gitextract_f2uvh5_n/
├── .github/
│ └── workflows/
│ ├── build.yaml
│ ├── codeql.yml
│ └── release.yaml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── .tool-versions
├── README.md
├── TODO.txt
├── Taskfile.yaml
├── go.mod
├── go.sum
├── main.go
├── pkg/
│ ├── config/
│ │ └── config.go
│ ├── install/
│ │ ├── install.go
│ │ └── install_test.go
│ ├── logging/
│ │ └── logging.go
│ ├── rm/
│ │ └── rm.go
│ ├── types/
│ │ ├── github.go
│ │ ├── github_test.go
│ │ └── os.go
│ └── utils/
│ ├── utils.go
│ └── utils_test.go
└── testdata/
└── helm-latest.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yaml
================================================
name: Build and Test
permissions:
contents: read
on:
pull_request:
branches: [ master ]
push:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Cache Go modules
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
run: go mod download
- name: Verify dependencies
run: go mod verify
- name: Install Task
uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Lint, Test, and Build
run: |
task ci
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: latest
args: --timeout=5m
================================================
FILE: .github/workflows/codeql.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '44 20 * * 3'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
- language: go
build-mode: autobuild
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
================================================
FILE: .github/workflows/release.yaml
================================================
name: goreleaser
on:
push:
tags:
- '*'
jobs:
goreleaser:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
kelp
build/
dist/
# llm files
.cursor
.claude
================================================
FILE: .golangci.yml
================================================
version: "2"
run:
go: "1.23"
linters:
enable:
- gocritic
- gosec
- misspell
- revive
disable:
- errcheck
settings:
gosec:
excludes:
- G404
revive:
rules:
- name: exported
disabled: false
- name: unreachable-code
disabled: false
- name: unused-parameter
disabled: false
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- gocritic
- gosec
path: _test\.go
paths:
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
================================================
FILE: .goreleaser.yaml
================================================
# GoReleaser configuration for kelp
version: 2
project_name: kelp
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
main: .
binary: kelp
ldflags:
- -s -w
- -X main.version={{.Version}}
- -X main.commit={{.Commit}}
- -X main.date={{.Date}}
gcflags:
- all=-l
- ./dontoptimizeme=-N
archives:
- formats: [tar.gz]
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- README.md
- LICENSE*
checksum:
name_template: "checksums.txt"
changelog:
sort: asc
use: github
filters:
exclude:
- "^docs:"
- "^test:"
- "^ci:"
- "^chore:"
- merge conflict
- Merge pull request
- Merge remote-tracking branch
- Merge branch
groups:
- title: "🚀 New Features"
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: "🐛 Bug Fixes"
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: "📚 Documentation"
regexp: "^.*docs[(\\w)]*:+.*$"
order: 2
- title: "🔧 Improvements"
regexp: "^.*refactor[(\\w)]*:+.*$"
order: 3
- title: "Other Changes"
order: 999
release:
github:
owner: crhuber
name: kelp
draft: false
prerelease: auto
mode: replace
header: |
## kelp {{ .Tag }}
This release includes kelp.
### Installation
Download the appropriate binary for your platform and follow the installation instructions in the README.
================================================
FILE: .tool-versions
================================================
golang 1.24.0
================================================
FILE: README.md
================================================
# KELP
<p align="center">
<img height="150px" src="./logo.png" alt="KELP" title="KELP">
</p>
## What is it
A simple replacement for homebrew for installing binary packages on MacOS & Linux written in Go.
## How Does it Work?
1. It downloads a Github releases package matching your operating system and architecture to `~/.kelp/cache`
2. It extracts any binary files to `~/.kelp/bin`
3. It unquarantines the binary (mac)
Example:
```
kelp update --install crumb
🌐 Getting releases for crhuber/crumb:latest...
Latest release v0.0.24. Kelp configured release v0.0.21.
===> Installing crhuber/crumb:v0.0.24...
🌐 Getting releases by tag v0.0.24...
🍏 Finding assets to download...
===> Downloading https://api.github.com/repos/crhuber/crumb/releases/assets/372978000...
Downloading 100%
📂 Extracting /Users/Craig/.kelp/cache/crumb_0.0.24_darwin_arm64.tar.gz
🧐 Checking for binary files in extract...
💾 Copying crumb to kelp bin...
✅ Installed crumb !
🛃 Unquarantining /Users/Craig/.kelp/bin/crumb..
```
## Why?
I built Kelp to scratch my own itch:
* No waiting for a formula to become available on homebrew
* Keep all your computers up to date with a single installation manifest
* No homebrew auto update
* No bloat, no magic
Couldn't this just be a bash script? Probably.
## How To Install
Go to the [releases](https://github.com/crhuber/kelp/releases) page. Download the latest release
Add kelp binary path to your PATH
```
export PATH=~/.kelp/bin/:$PATH
```
## Quick Setup
1. Initialize Kelp
```
kelp init
```
2. Add a new package
```
kelp add junegunn/fzf
```
To use a specific version use the `-r` flag. Where `-r` is the github release version
```
kelp add junegunn/fzf -r 1.0.0
```
3. Install
```
kelp install fzf
```
or
```
kelp add junegunn/fzf --install
```
### Updating a Package
Update
```
kelp update fzf
kelp install fzf
```
Update and install
```
kelp update fzf --install
```
Update to a specific version
```
kelp set fzf -r v0.72.0
kelp install fzf
```
## Configuration
```
GLOBAL OPTIONS:
--config string, -c string path to kelp config file (default: "/Users/Craig/.kelp/kelp.json") [$KELP_CONFIG]
--verbose verbose output [$KELP_VERBOSE]
```
## Troubleshooting
### What if the package I want is not on github releases?
Just kelp add the link to the binary, ie:
```
kelp add hashicorp/terraform -r https://releases.hashicorp.com/terraform/0.11.13/terraform_0.11.13_darwin_amd64.zip
```
Also supported are packages like `helm` that provide external non-github download links in the release description.
### Increase logging level
Use `--verbose` or set `KELP_VERBOSE=1` to get more information, expecially during the installation process.
### Why wasnt my package installed ?
Kelp looks for binaries made for MacOS or Linux. If it cannot file a suitable binary for your machine architecture and operating system it will skip downloading it or wont extract it.
Use inspect to open the cache and bin directories for your package
```
kelp inspect
```
To see what binaries exist use:
```
kelp doctor
```
If your binary has a different filename than the name of the Github project, kelp doctor may not find it. To give it a hint you can add the name of the binary to the kelp config
```
kelp set jira-cli -b "jira"`
```
To see whats in your config use:
```
kelp ls
```
### Does it work for Linux?
Yes!
### What if I'm rate limited by Github Api?
Set a github token environment variable
```
export GITHUB_TOKEN="XYZ"
```
## Contributing
If you find bugs, please open an issue first. If you have feature requests, I may not honor it because this project is being built mostly to suit my personal workflow and preferences.
================================================
FILE: TODO.txt
================================================
- when file already exists in cache do a hash before skipping it
- add post install hooks
================================================
FILE: Taskfile.yaml
================================================
version: '3'
vars:
BINARY_NAME: kelp
BUILD_DIR: ./build
COVERAGE_DIR: ./coverage
tasks:
default:
desc: Show available tasks
cmds:
- task --list
clean:
desc: Clean build artifacts and coverage reports
cmds:
- rm -rf {{.BUILD_DIR}}
- rm -rf {{.COVERAGE_DIR}}
- rm -f {{.BINARY_NAME}}
- rm -f coverage.out coverage.html
deps:
desc: Download and verify dependencies
cmds:
- go mod download
- go mod verify
build:
desc: Build the application
deps: [clean]
cmds:
- mkdir -p {{.BUILD_DIR}}
- go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}} .
test:
desc: Run all tests
cmds:
- echo "Running unit tests..."
- go test -v ./...
test-unit:
desc: Run unit tests only
cmds:
- echo "Running unit tests..."
- go test -v ./... -run "^Test"
test-integration:
desc: Run integration tests only
cmds:
- echo "Running integration tests..."
- go test -v ./... -run "Integration"
test-coverage:
desc: Run tests with coverage report
cmds:
- mkdir -p {{.COVERAGE_DIR}}
- go test -v -cover -coverprofile={{.COVERAGE_DIR}}/coverage.out ./...
- go tool cover -html={{.COVERAGE_DIR}}/coverage.out -o {{.COVERAGE_DIR}}/coverage.html
- 'echo "Coverage report generated: {{.COVERAGE_DIR}}/coverage.html"'
test-coverage-text:
desc: Run tests with coverage report (text output)
cmds:
- go test -v -cover -coverprofile=coverage.out ./...
- go tool cover -func=coverage.out
- rm -f coverage.out
lint:
desc: Run linter
cmds:
- echo "Running linter..."
- go vet ./...
- go fmt ./...
- golangci-lint run --timeout=5m
lint-check:
desc: Check code formatting and linting
cmds:
- echo "Checking code formatting..."
- test -z "$(go fmt ./...)"
- go vet ./...
security:
desc: Run security scanner (requires gosec)
cmds:
- echo "Running security scanner..."
- gosec ./...
preconditions:
- sh: command -v gosec
msg: "gosec is not installed. Run: go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest"
ci:
desc: Run CI pipeline (lint, test, build)
cmds:
- task: lint-check
- task: test-coverage-text
- task: build
ci-full:
desc: Run full CI pipeline with benchmarks
cmds:
- task: deps
- task: lint-check
- task: test-coverage
- task: build
setup-dev:
desc: Setup development environment
cmds:
- echo "Setting up development environment..."
- go mod download
- go install github.com/securecodewarrior/gosec/v2/cmd/gosec@latest
- go install github.com/go-task/task/v3/cmd/task@latest
- echo "Development environment setup complete!"
test-race:
desc: Run tests with race detection
cmds:
- echo "Running tests with race detection..."
- go test -v -race ./...
release:
desc: Build release binaries for multiple platforms
deps: [clean, test-coverage-text, lint-check]
cmds:
- mkdir -p {{.BUILD_DIR}}/release
- echo "Building release binaries..."
- GOOS=darwin GOARCH=amd64 go build -o {{.BUILD_DIR}}/release/{{.BINARY_NAME}}-darwin-amd64 .
- GOOS=darwin GOARCH=arm64 go build -o {{.BUILD_DIR}}/release/{{.BINARY_NAME}}-darwin-arm64 .
- GOOS=linux GOARCH=amd64 go build -o {{.BUILD_DIR}}/release/{{.BINARY_NAME}}-linux-amd64 .
- GOOS=linux GOARCH=arm64 go build -o {{.BUILD_DIR}}/release/{{.BINARY_NAME}}-linux-arm64 .
- echo "Release binaries built in {{.BUILD_DIR}}/release/"
================================================
FILE: go.mod
================================================
module crhuber/kelp
go 1.23.0
toolchain go1.23.8
require (
github.com/gabriel-vasile/mimetype v1.4.12
github.com/h2non/gock v1.2.0
github.com/mholt/archives v0.1.4
github.com/schollz/progressbar/v3 v3.19.0
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.6.1
)
require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/nwaples/rardecode/v2 v2.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.23 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mholt/archives v0.1.4 h1:sU+/lLNgafUontWFv3AVwO8VUWye3rrtN6hgC2dU11c=
github.com/mholt/archives v0.1.4/go.mod h1:I2ia+SQTtQHej9w1GZM/mz7qfdgQv+BHr3hEKqDcGuk=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/nwaples/rardecode/v2 v2.1.1 h1:OJaYalXdliBUXPmC8CZGQ7oZDxzX1/5mQmgn0/GASew=
github.com/nwaples/rardecode/v2 v2.1.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU=
github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
================================================
FILE: main.go
================================================
package main
import (
"context"
"crhuber/kelp/pkg/config"
"crhuber/kelp/pkg/install"
"crhuber/kelp/pkg/logging"
"crhuber/kelp/pkg/types"
"crhuber/kelp/pkg/utils"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/urfave/cli/v3"
)
var (
version = "dev"
)
func main() {
// default config
var home, _ = os.UserHomeDir()
var KelpConf = filepath.Join(home, "/.kelp/kelp.json")
if types.GetCapabilities() == nil {
fmt.Println("Sorry, your OS is not yet supported.")
os.Exit(1)
}
app := &cli.Command{
Name: "kelp",
Version: version,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Value: KelpConf,
Usage: "path to kelp config file",
Sources: cli.EnvVars("KELP_CONFIG"),
},
&cli.BoolFlag{
Name: "verbose",
Value: false,
Usage: "verbose output",
Sources: cli.EnvVars("KELP_VERBOSE"),
Action: func(_ context.Context, _ *cli.Command, val bool) error {
logging.SetLogVerbose(val)
return nil
},
},
},
Commands: []*cli.Command{
{
Name: "add",
Usage: "add a new package to config",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "release",
Aliases: []string{"r"},
Value: "latest",
Usage: "release for package",
},
&cli.BoolFlag{
Name: "install",
Aliases: []string{"i"},
Value: false,
Usage: "also install package",
},
},
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
ownerRepo := strings.Split(project, "/")
if len(ownerRepo) < 2 {
return fmt.Errorf("use owner/repo format")
}
// resolve release version
releaseFlag := cmd.String("release")
var actualRelease string
if releaseFlag == "latest" {
// Get the actual latest release version from GitHub
latestRelease, err := utils.GetGithubRelease(ownerRepo[0], ownerRepo[1], "latest")
if err != nil {
return fmt.Errorf("failed to get latest release for %s/%s: %s", ownerRepo[0], ownerRepo[1], err)
}
actualRelease = latestRelease.TagName
} else {
actualRelease = releaseFlag
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
err = kc.AddPackage(ownerRepo[0], ownerRepo[1], actualRelease)
if err != nil {
return fmt.Errorf("%s", err)
}
// save config
err = kc.Save()
if err != nil {
return fmt.Errorf("%s", err)
}
// auto install
if cmd.Bool("install") {
err = install.Install(ownerRepo[0], ownerRepo[1], actualRelease)
if err != nil {
return err
}
}
return nil
},
},
{
Name: "browse",
Usage: "browse to project github page",
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
p, err := kc.GetPackage(project)
if err != nil {
return fmt.Errorf("%s", err)
}
config.Browse(p.Owner, p.Repo)
return nil
},
},
{
Name: "doctor",
Usage: "checks if packages are installed properly",
Action: func(_ context.Context, cmd *cli.Command) error {
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
kc.Doctor()
return nil
},
},
{
Name: "get",
Usage: "get package details",
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
p, err := kc.GetPackage(project)
if err != nil {
return fmt.Errorf("%s", err)
}
fmt.Printf("[%s/%s]\n", p.Owner, p.Repo)
fmt.Printf("Release: %s\n", p.Release)
fmt.Printf("Description: %s\n", p.Description)
fmt.Printf("Url: https://github.com/%s/%s\n", p.Owner, p.Repo)
fmt.Printf("Binary: %s\n", p.Binary)
fmt.Printf("Updated At: %s\n", p.UpdatedAt)
return nil
},
},
{
Name: "init",
Usage: "initialize kelp",
Action: func(_ context.Context, cmd *cli.Command) error {
err := config.Initialize(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
return nil
},
},
{
Name: "inspect",
Usage: "inspect kelp bin directory",
Action: func(_ context.Context, _ *cli.Command) error {
config.Inspect()
return nil
},
},
{
Name: "install",
Usage: "install kelp package",
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
kp, err := kc.GetPackage(project)
if err != nil {
return fmt.Errorf("%s", err)
}
err = install.Install(kp.Owner, kp.Repo, kp.Release)
if err != nil {
return err
}
return nil
},
},
{
Name: "list",
Aliases: []string{"ls"},
Usage: "list kelp packages",
Action: func(_ context.Context, cmd *cli.Command) error {
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
kc.List()
return nil
},
},
{
Name: "remove",
Aliases: []string{"rm"},
Usage: "remove a package from config and disk",
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
kp, err := kc.GetPackage(project)
if err != nil {
return fmt.Errorf("%s", err)
}
// remove from config
err = kc.RemovePackage(kp.Repo)
if err != nil {
return fmt.Errorf("%s", err)
}
// save config
err = kc.Save()
if err != nil {
return fmt.Errorf("error saving: %s", err)
}
return nil
},
},
{
Name: "set",
Usage: "set package configuration in config",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "release",
Aliases: []string{"r"},
Value: "latest",
Usage: "release for package",
},
&cli.StringFlag{
Name: "description",
Aliases: []string{"d"},
Value: "",
Usage: "description of package",
},
&cli.StringFlag{
Name: "binary",
Aliases: []string{"b"},
Value: "",
Usage: "alias of binary",
},
},
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
err = kc.SetPackage(project, cmd.String("release"), cmd.String("description"), cmd.String("binary"))
if err != nil {
return fmt.Errorf("%s", err)
}
// save config
err = kc.Save()
if err != nil {
return fmt.Errorf("%s", err)
}
return nil
},
},
{
Name: "update",
Usage: "update kelp package in config",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "install",
Aliases: []string{"i"},
Value: false,
Usage: "also install package",
},
},
Action: func(_ context.Context, cmd *cli.Command) error {
project := cmd.Args().First()
if project == "" {
return errors.New("project argument required")
}
// load config
kc, err := config.Load(cmd.String("config"))
if err != nil {
return fmt.Errorf("%s", err)
}
kp, err := kc.GetPackage(project)
if err != nil {
return fmt.Errorf("%s", err)
}
// handle http packages
if strings.HasPrefix(kp.Release, "http") {
return errors.New("update functionality not supported for http packages")
}
ghr, err := utils.GetGithubRelease(kp.Owner, kp.Repo, "latest")
if err != nil {
return fmt.Errorf("%s", err)
}
if ghr.TagName == kp.Release {
logging.LogInfo("Latest release %s already matches release %s in kelp config", ghr.TagName, kp.Release)
return nil
}
logging.LogInfo("Latest release %s. Kelp configured release %s. Update config [y/n] ? : ", ghr.TagName, kp.Release)
var confirmation string
confirmation = strings.TrimSpace(confirmation)
confirmation = strings.ToLower(confirmation)
// Taking input from user
fmt.Scanln(&confirmation)
if confirmationUpper := strings.ToUpper(confirmation); confirmationUpper == "Y" || confirmationUpper == "YES" {
err = kc.SetPackage(kp.Repo, ghr.TagName, "", "")
if err != nil {
return fmt.Errorf("%s", err)
}
// save config
err = kc.Save()
if err != nil {
return fmt.Errorf("%s", err)
}
}
// auto install
if cmd.Bool("install") {
err = install.Install(kp.Owner, kp.Repo, ghr.TagName)
if err != nil {
return err
}
}
return nil
},
},
},
}
if err := app.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}
================================================
FILE: pkg/config/config.go
================================================
package config
import (
"crhuber/kelp/pkg/logging"
"crhuber/kelp/pkg/types"
"crhuber/kelp/pkg/utils"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"text/tabwriter"
"time"
)
var home, _ = os.UserHomeDir()
var KelpDir = filepath.Join(home, "/.kelp/")
var KelpBin = filepath.Join(home, "/.kelp/bin/")
var KelpCache = filepath.Join(home, "/.kelp/cache/")
type KelpConfig struct {
Path string `json:"-"`
Packages []KelpPackage
}
type KelpPackage struct {
Owner string `json:"Owner"`
Repo string `json:"Repo"`
Release string `json:"Release"`
UpdatedAt time.Time `json:"UpdatedAt"`
Description string `json:"Description"`
Binary string `json:"Binary"`
}
func (kc *KelpConfig) Pop(index int) []KelpPackage {
return append(kc.Packages[:index], kc.Packages[index+1:]...)
}
func (kc *KelpConfig) GetPackage(repo string) (*KelpPackage, error) {
parts := strings.Split(repo, "/")
// Check if there is an owner part since some projects have the same repo name
// like cli
if len(parts) > 1 {
// If there is an owner, get the more specific project first
for _, kp := range kc.Packages {
if kp.Owner == parts[0] && kp.Repo == parts[1] {
return &kp, nil
}
}
} else {
for _, kp := range kc.Packages {
if kp.Repo == repo {
return &kp, nil
}
}
}
return nil, errors.New("package not found in config, try adding it first")
}
func Load(path string) (*KelpConfig, error) {
bs, _ := os.ReadFile(path)
kc := KelpConfig{}
err := json.Unmarshal(bs, &kc.Packages)
if err != nil {
return nil, err
}
kc.Path = path
return &kc, nil
}
func (kc *KelpConfig) Save() error {
bs, _ := json.MarshalIndent(kc.Packages, "", " ")
err := os.WriteFile(kc.Path, bs, 0600)
if err != nil {
return err
}
logging.LogInfo("Config saved.")
return nil
}
func (kc *KelpConfig) RemovePackage(repo string) error {
for i, kp := range kc.Packages {
if kp.Repo == repo {
kc.Packages = kc.Pop(i)
logging.LogInfo("Package %s removed\n", repo)
return nil
}
}
return errors.New("package not found in config")
}
func (kc *KelpConfig) AddPackage(owner, repo, release string) error {
for _, p := range kc.Packages {
if p.Owner == owner && p.Repo == repo {
return fmt.Errorf("package already exists in config")
}
}
// append a new item
kp := KelpPackage{
Owner: owner,
Repo: repo,
Release: release,
UpdatedAt: time.Now(),
}
kc.Packages = append(kc.Packages, kp)
logging.LogDebug("Config added for %s/%s", owner, repo)
return nil
}
func (kc *KelpConfig) UpdatePackage(repo string) (string, error) {
for _, p := range kc.Packages {
if p.Repo == repo {
ghr, err := utils.GetGithubRelease(p.Owner, p.Repo, "latest")
if err != nil {
return "", err
}
return ghr.TagName, nil
}
}
return "", errors.New("package not found in config")
}
func (kc *KelpConfig) SetPackage(repo, release, description, binary string) error {
for i, p := range kc.Packages {
if p.Repo == repo {
if release != "" {
kc.Packages[i].Release = release
kc.Packages[i].UpdatedAt = time.Now()
}
if description != "" {
kc.Packages[i].Description = description
}
if binary != "" {
kc.Packages[i].Binary = binary
}
logging.LogDebug("Config set for %s", repo)
return nil
}
}
return nil
}
func (kc *KelpConfig) List() {
w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0)
// sort by date
sort.Slice(kc.Packages, func(i, j int) bool {
return kc.Packages[i].UpdatedAt.Before(kc.Packages[j].UpdatedAt)
})
for _, pkg := range kc.Packages {
// Format the timestamp in a more human-friendly way
humanFriendlyTimestamp := pkg.UpdatedAt.Format("Jan 2 2006")
if humanFriendlyTimestamp == "Jan 1 0001" {
humanFriendlyTimestamp = ""
}
release := ""
if strings.HasPrefix(pkg.Release, "http") {
// Define the regex pattern to extract version numbers
pattern := `[/v-]([\d.]+)`
// Compile the regex pattern
re := regexp.MustCompile(pattern)
match := re.FindStringSubmatch(pkg.Release)
if len(match) > 1 {
release = fmt.Sprintf("%s (https)", match[1])
} else {
release = "unknown (https)"
}
} else {
release = pkg.Release
}
fmt.Fprintf(w, "%s/%s\t%s\t%s\n", pkg.Owner, pkg.Repo, release, humanFriendlyTimestamp)
}
w.Flush()
}
func Initialize(path string) error {
if !utils.DirExists(KelpDir) {
logging.LogDebug("Creating Kelp dir...")
err := os.Mkdir(KelpDir, 0777)
if err != nil {
return err
}
}
if !utils.DirExists(KelpCache) {
logging.LogDebug("Creating Kelp cache...")
err := os.Mkdir(KelpCache, 0777)
if err != nil {
return err
}
}
if !utils.DirExists(KelpBin) {
logging.LogDebug("Creating Kelp bin...")
err := os.Mkdir(KelpBin, 0777)
if err != nil {
return err
}
}
// create empty config
kp := KelpPackage{
Owner: "crhuber",
Repo: "kelp",
Release: "latest",
UpdatedAt: time.Now(),
Description: "Simple homebrew alternative",
}
kc := KelpConfig{
Path: path,
Packages: []KelpPackage{kp},
}
if !utils.FileExists(path) {
logging.LogDebug("Creating Kelp config file...")
err := kc.Save()
if err != nil {
return err
}
} else {
logging.LogDebug("Skipping Kelp config file creation since one alredy exists...")
}
logging.LogInfo("🌱 Kelp Initialized!")
logging.LogInfo("🗒 Add Kelp to your path by running: \nexport PATH=%s:$PATH >> ~/.bash_profile\n", KelpBin)
return nil
}
func Inspect() {
var err error
switch types.GetOS() {
case types.Darwin:
err = exec.Command("open", KelpDir).Start()
case types.Linux:
err = exec.Command("xdg-open", KelpDir).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Fatal(err)
}
}
func Browse(owner, repo string) {
var err error
url := fmt.Sprintf("https://github.com/%s/%s", owner, repo)
logging.LogDebug("Opening %s\n", url)
switch types.GetOS() {
case types.Darwin:
err = exec.Command("open", url).Start()
case types.Linux:
err = exec.Command("xdg-open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Fatal(err)
}
}
func (kc *KelpConfig) Doctor() {
w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0)
for _, p := range kc.Packages {
// check alias first
var binary string
if p.Binary != "" {
binary = p.Binary
} else {
binary = p.Repo
}
status := ""
path, err := commandExists(binary)
if err != nil {
status = "❌ Binary not found"
} else {
if strings.HasPrefix(path, KelpBin) {
status = "✅ Installed"
} else {
status = "⛔️ Installed outside kelp"
}
}
logging.LogInfo("%s\t%s\n", binary, status)
}
w.Flush()
}
func commandExists(cmd string) (string, error) {
return exec.LookPath(cmd)
}
================================================
FILE: pkg/install/install.go
================================================
package install
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"crhuber/kelp/pkg/config"
"crhuber/kelp/pkg/logging"
"crhuber/kelp/pkg/types"
"crhuber/kelp/pkg/utils"
"github.com/gabriel-vasile/mimetype"
"github.com/mholt/archives"
"github.com/schollz/progressbar/v3"
)
func Install(owner, repo, release string) error {
// handle http packages
tempdir, _ := os.MkdirTemp("", "kelp")
defer os.RemoveAll(tempdir)
var downloadPath string
if strings.HasPrefix(release, "http") {
urlsplit := strings.SplitAfter(release, "/")
filename := urlsplit[len(urlsplit)-1]
downloadPath = filepath.Join(config.KelpCache, filename)
err := downloadFile(downloadPath, release)
if err != nil {
return err
}
} else {
asset, err := downloadGithubRelease(owner, repo, release)
if err != nil {
return err
}
downloadPath = filepath.Join(config.KelpCache, asset.Name)
}
err := extractPackage(downloadPath, tempdir)
if err != nil {
return err
}
destinations := installBinary(tempdir)
if types.IsDarwin() {
for _, d := range destinations {
unquarantineFile(d)
}
}
return nil
}
func unquarantineFile(filepath string) error {
logging.LogInfo("🛃 Unquarantining %s...\n", filepath)
cmd := exec.Command("xattr", "-d", "com.apple.quarantine", filepath)
return cmd.Run()
}
// downloadFile downloads files
func downloadFile(filepath string, url string) error {
logging.LogInfo("===> Downloading %s...\n", url)
logging.LogDebug("To: %s...\n", filepath)
// Get the data
req, _ := http.NewRequest("GET", url, nil)
// set headers for github auth
if ghToken := os.Getenv("GITHUB_TOKEN"); ghToken != "" {
logging.LogDebug("Using Github token in http request")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ghToken))
}
req.Header.Set("Accept", "application/octet-stream")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("\ninvalid HTTP status: %v", resp.StatusCode)
}
defer resp.Body.Close()
// Create the file
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
// Write the body to file
bar := progressbar.DefaultBytes(
resp.ContentLength,
"Downloading",
)
_, err = io.Copy(io.MultiWriter(out, bar), resp.Body)
return err
}
func extractPackage(downloadPath, tempDir string) error {
logging.LogInfo("📂 Extracting %s\n", downloadPath)
// Handle dmg files
if strings.HasSuffix(downloadPath, ".dmg") {
return errors.New("kelp does not support dmg files")
}
// Open the file
file, err := os.Open(downloadPath)
if err != nil {
return fmt.Errorf("could not open file: %w", err)
}
defer file.Close()
// Try to identify archive format
ctx := context.Background()
format, stream, err := archives.Identify(ctx, downloadPath, file)
if err != nil {
// Not a recognized archive — treat as raw binary
logging.LogDebug("File is not a recognized archive format. Treating as raw binary.")
cleanName := cleanBinaryName(filepath.Base(downloadPath))
destPath := filepath.Join(tempDir, cleanName)
if copyErr := utils.CopyFile(downloadPath, destPath); copyErr != nil {
return fmt.Errorf("could not copy binary to temp dir: %w", copyErr)
}
os.Chmod(destPath, 0o755)
return nil
}
// Check if the format supports extraction
extractor, ok := format.(archives.Extractor)
if !ok {
return fmt.Errorf("archive format does not support extraction")
}
// Extract all files to destination directory
err = extractor.Extract(ctx, stream, func(_ context.Context, f archives.FileInfo) error {
return extractFile(f, tempDir)
})
if err != nil {
return fmt.Errorf("extraction failed: %w", err)
}
return nil
}
// cleanBinaryName strips OS/arch suffixes from binary filenames.
// For example, "direnv.darwin-arm64" becomes "direnv".
func cleanBinaryName(name string) string {
osNames := []string{"darwin", "linux", "macos", "windows"}
archNames := []string{"arm64", "aarch64", "amd64", "x86_64", "x64"}
lower := strings.ToLower(name)
for _, osName := range osNames {
for _, arch := range archNames {
for _, sep := range []string{"-", "_"} {
// os-arch: direnv.darwin-arm64
suffix := "." + osName + sep + arch
if strings.HasSuffix(lower, suffix) {
return name[:len(name)-len(suffix)]
}
// arch-os: direnv.arm64-darwin
suffix = "." + arch + sep + osName
if strings.HasSuffix(lower, suffix) {
return name[:len(name)-len(suffix)]
}
}
}
}
return name
}
// Helper function to extract a single file
func extractFile(f archives.FileInfo, destDir string) error {
extractPath := filepath.Join(destDir, f.NameInArchive)
if f.IsDir() {
return os.MkdirAll(extractPath, f.Mode())
}
if err := os.MkdirAll(filepath.Dir(extractPath), 0o755); err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
outFile, err := os.OpenFile(extractPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer outFile.Close()
_, err = io.Copy(outFile, rc)
return err
}
func installBinary(tempDir string) []string {
logging.LogInfo("🧐 Checking for binary files in extract...")
files, err := utils.FilePathWalkDir(tempDir)
if err != nil {
log.Panic("Could not walk directory")
}
destinations := []string{}
osCap := types.GetCapabilities()
var foundLibs []string
for _, file := range files {
mime, _ := mimetype.DetectFile(string(file))
// only install binary files
switch mime.String() {
case osCap.ExecutableMime:
destinations = append(destinations, copyToKelpBin(file))
case osCap.SharedLibrary:
splits := strings.SplitAfter(file, "/")
fileName := splits[len(splits)-1]
logging.LogDebug("Shared/Static Library file %s found in extract.\n", fileName)
foundLibs = append(foundLibs, file)
default:
logging.LogDebug("Skipping non executable file: %v - %v\n", file, mime.String())
}
}
if len(destinations) == 0 { // if no binary was found in extract, then filter shared libararies
if len(foundLibs) == 1 {
destinations = append(destinations, copyToKelpBin(foundLibs[0]))
} else {
var filteredLibs []string
for _, currentLib := range foundLibs {
if !strings.HasPrefix(currentLib, "lib") && !strings.HasSuffix(currentLib, "dynlib") {
filteredLibs = append(filteredLibs, currentLib)
} else {
mime, _ := mimetype.DetectFile(string(currentLib))
logging.LogDebug("Skipping non executable file: %v - %v\n", currentLib, mime.String())
}
}
if len(filteredLibs) == 1 {
destinations = append(destinations, copyToKelpBin(filteredLibs[0]))
} else {
for _, currentUnrecognizedLib := range filteredLibs {
mime, _ := mimetype.DetectFile(string(currentUnrecognizedLib))
logging.LogDebug("Skipping non executable file: %v - %v\n", currentUnrecognizedLib, mime.String())
}
}
}
}
return destinations
}
func copyToKelpBin(file string) string {
splits := strings.SplitAfter(file, "/")
fileName := splits[len(splits)-1]
logging.LogDebug("Binary file %s found in extract.\n", fileName)
destination := filepath.Join(config.KelpBin, fileName)
logging.LogInfo("💾 Copying %v to kelp bin...\n", fileName)
utils.CopyFile(file, destination)
logging.LogInfo("✅ Installed %v !\n", fileName)
return destination
}
func downloadGithubRelease(owner, repo, release string) (*types.Asset, error) {
logging.LogInfo("===> Installing %s/%s:%s...\n", owner, repo, release)
ghr, err := utils.GetGithubRelease(owner, repo, release)
if err != nil {
return nil, err
}
logging.LogInfo("🍏 Finding assets to download...")
downloadableAsset, err := ghr.FindBestAsset(types.GetCapabilities())
if err != nil {
return nil, err
}
downloadPath := filepath.Join(config.KelpCache, downloadableAsset.Name)
if utils.FileExists(downloadPath) {
logging.LogDebug("File %v already exists in cache, skipping download.\n", downloadableAsset.Name)
} else {
err := downloadFile(downloadPath, downloadableAsset.URL)
if err != nil {
return nil, err
}
}
return downloadableAsset, nil
}
================================================
FILE: pkg/install/install_test.go
================================================
package install
import "testing"
func TestCleanBinaryName(t *testing.T) {
tests := []struct {
input string
want string
}{
{"direnv.darwin-arm64", "direnv"},
{"direnv.darwin-amd64", "direnv"},
{"direnv.linux-arm64", "direnv"},
{"direnv.linux-amd64", "direnv"},
{"tool.darwin_arm64", "tool"},
{"tool.linux_amd64", "tool"},
{"tool.arm64-darwin", "tool"},
{"tool.amd64-linux", "tool"},
{"tool.macos-arm64", "tool"},
{"tool.windows-x86_64", "tool"},
{"tool.linux-aarch64", "tool"},
{"tool.darwin-x64", "tool"},
// No matching suffix — returned as-is
{"mybinary", "mybinary"},
{"archive.tar.gz", "archive.tar.gz"},
{"tool.v1.2.3", "tool.v1.2.3"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := cleanBinaryName(tt.input)
if got != tt.want {
t.Errorf("cleanBinaryName(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
================================================
FILE: pkg/logging/logging.go
================================================
package logging
import "log"
var logVerbose = false
func SetLogVerbose(verbose bool) {
logVerbose = verbose
}
func LogDebug(message string, args ...any) {
if logVerbose {
log.Printf(message, args...)
}
}
func LogInfo(message string, args ...any) {
log.Printf(message, args...)
}
func init() {
// set log output to the simplest as possible, without anything but the message
log.SetFlags(0)
}
================================================
FILE: pkg/rm/rm.go
================================================
package rm
import (
"crhuber/kelp/pkg/config"
"crhuber/kelp/pkg/logging"
"crhuber/kelp/pkg/utils"
"os"
"path/filepath"
)
func RemoveBinary(binary string) error {
binaryPath := filepath.Join(config.KelpBin, binary)
if utils.FileExists(binaryPath) {
logging.LogInfo("Removing binary %s...", binary)
return os.Remove(binaryPath)
}
return nil
}
================================================
FILE: pkg/types/github.go
================================================
package types
import (
"crhuber/kelp/pkg/logging"
"errors"
"regexp"
"sort"
"strings"
"time"
)
// Asset represents a downloadable asset from a Github release
type Asset struct {
URL string `json:"url"`
ID int `json:"id"`
Name string `json:"name"`
Label string `json:"label"`
ContentType string `json:"content_type"`
State string `json:"state"`
Size int `json:"size"`
DownloadCount int `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
BrowserDownloadURL string `json:"browser_download_url"`
}
// GithubRelease represents a Github release
type GithubRelease struct {
URL string `json:"url"`
AssetsURL string `json:"assets_url"`
UploadURL string `json:"upload_url"`
HTMLURL string `json:"html_url"`
ID int `json:"id"`
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish"`
Name string `json:"name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
Assets []Asset `json:"assets"`
Body string `json:"body"`
}
// methods
func (a *Asset) isDownloadableExtension() bool {
downLoadableExtension := []string{".zip", ".tar", ".gz", ".xz", ".dmg", ".pkg", ".tgz", ".bz2"}
for _, word := range downLoadableExtension {
result := strings.HasSuffix(a.BrowserDownloadURL, word)
if result {
return result
}
}
return false
}
func (a *Asset) isChecksumFile() bool {
checksumExtension := []string{".asc", ".sha256.asc", ".sha512.asc", ".sha256sum.asc", ".sha512sum.asc", ".sha1.asc", ".md5.asc"}
for _, word := range checksumExtension {
if strings.HasSuffix(a.BrowserDownloadURL, word) {
return true
}
}
return false
}
func (a *Asset) hasNoExtension() bool {
bdu := strings.SplitAfter(a.BrowserDownloadURL, "/")
filename := bdu[len(bdu)-1]
return !strings.Contains(filename, ".")
}
// IsMacAsset checks if the download url contains "mac", "macos", "darwin", "osx", "apple" and returns true if so
func (a *Asset) isMacAsset() bool {
macIdentifiers := []string{"mac", "macos", "darwin", "osx", "apple"}
for _, word := range macIdentifiers {
result := strings.Contains(strings.ToLower(a.BrowserDownloadURL), word)
if result {
return result
}
}
return false
}
func (a *Asset) isLinuxAsset() bool {
macIdentifiers := []string{"linux"}
for _, word := range macIdentifiers {
result := strings.Contains(strings.ToLower(a.BrowserDownloadURL), word)
if result {
return result
}
}
return false
}
func (a *Asset) isSameOS(capabilities *Capabilities) bool {
switch capabilities.OS {
case Darwin:
return a.isMacAsset()
case Linux:
return a.isLinuxAsset()
}
return false
}
func (a *Asset) isSameArchitecture(capabilities *Capabilities) bool {
lowerURL := strings.ToLower(a.BrowserDownloadURL)
// First check if the URL contains the exact arch name
if strings.Contains(lowerURL, strings.ToLower(capabilities.Arch)) {
return true
}
// Then handle architecture aliases
switch capabilities.Arch {
case "amd64":
return strings.Contains(lowerURL, "x86_64")
case "arm64":
return strings.Contains(lowerURL, "arm64") || strings.Contains(lowerURL, "aarch64")
default:
return false
}
}
const (
MIN_ASSET_SCORE = 6 // minimum score for an asset to be considered suitable for download
)
func (a *Asset) EvaluateSuitability(capabilities *Capabilities) int {
assetScore := 0
if a.isSameOS(capabilities) {
assetScore += 4
}
if a.isSameArchitecture(capabilities) {
assetScore += 3
}
if a.isDownloadableExtension() {
assetScore += 2
}
if a.hasNoExtension() {
assetScore += 1
}
if a.isChecksumFile() {
assetScore -= 10
}
return assetScore
}
func (a *Asset) RealFilename() string {
if a.Name != "" {
return a.Name
}
url := a.URL
if url == "" {
url = a.BrowserDownloadURL
}
filename := strings.Split(url, "/")
return filename[len(filename)-1]
}
// A data structure to hold key/value pairs
type Pair struct {
Key int
Value int
}
// A slice of pairs that implements sort.Interface to sort by values
type PairList []Pair
func (p PairList) Len() int { return len(p) }
func (p PairList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p PairList) Less(i, j int) bool { return p[i].Value < p[j].Value }
func (ghr *GithubRelease) FindBestAsset(capabilities *Capabilities) (*Asset, error) {
var bestAsset Asset
assetScores := map[int]int{}
for index, asset := range ghr.Assets {
if assetScore := asset.EvaluateSuitability(capabilities); assetScore >= MIN_ASSET_SCORE {
logging.LogDebug("Found suitable candidate %v for download. Score: %v", asset.RealFilename(), assetScore)
assetScores[index] = assetScore
}
}
if len(assetScores) == 0 {
// inspect the release body for links to downloadable assets
links := ghr.inspectLinksInReleaseBody()
// create a list of assets from the links and evaluate them
assetsFromBodyScores := map[int]int{}
assetLinks := make([]Asset, len(links))
for index, link := range links {
filename := strings.Split(link, "/")
realFilename := filename[len(filename)-1]
a := Asset{
BrowserDownloadURL: link,
URL: link,
Name: realFilename,
}
assetLinks[index] = a
if assetScore := a.EvaluateSuitability(capabilities); assetScore >= MIN_ASSET_SCORE {
logging.LogDebug("Found suitable candidate %v for download in release body. Score: %v", realFilename, assetScore)
assetsFromBodyScores[index] = assetScore
}
}
if len(assetsFromBodyScores) == 0 {
return nil, errors.New("no suitable candidates found in release body")
}
// sort the map by value of score.
highest := getHighestScore(assetsFromBodyScores)
bestAsset = assetLinks[highest.Key]
} else {
// sort the map by value of score.
highest := getHighestScore(assetScores)
bestAsset = ghr.Assets[highest.Key]
}
logging.LogDebug("Adding highest ranked asset %v to download queue.", bestAsset.RealFilename())
return &bestAsset, nil
}
func getHighestScore(assetScores map[int]int) Pair {
// sort the map by value of score.
assetsByScore := make(PairList, len(assetScores))
i := 0
for k, v := range assetScores {
assetsByScore[i] = Pair{k, v}
i++
}
sort.Sort(assetsByScore)
// return highest
return assetsByScore[len(assetsByScore)-1]
}
func (ghr *GithubRelease) inspectLinksInReleaseBody() []string {
const (
NAME_REGEXP = `([a-z][a-z0-9_-]+?)`
ARCH_REGEXP = `[._-](amd64|x86_64|x64|arm64|aarch64)`
OS_REGEXP = `[._-]((unknown[._-])?(linux|linux-gnu|linux-musl))|((apple[._-])?(darwin|macos|osx))`
VERSION_REGEXP = `([_-]v?[0-9.]+)?`
SUFFIX_REGEXP = `([_-][a-z0-9_-]+)?`
EXTENSION_REGEXP = `(\.zip|\.tar\.gz|\.gz|\.tgz|\.tar\.xz|\.txz|\.tar\.bz2|\.tbz)?`
REGEXP = NAME_REGEXP + VERSION_REGEXP + "(" + OS_REGEXP + ARCH_REGEXP + "|" + ARCH_REGEXP + OS_REGEXP + ")" + SUFFIX_REGEXP + EXTENSION_REGEXP
)
re := regexp.MustCompile(`https:\/\/[a-z0-9.\/]+\/` + REGEXP)
matches := re.FindAllString(ghr.Body, -1)
// remove duplicates
sort.Strings(matches)
// remove duplicates
seen := make(map[string]bool)
result := make([]string, 0)
for _, item := range matches {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
================================================
FILE: pkg/types/github_test.go
================================================
package types
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func TestEvalAssetSuitabilityDarwin(t *testing.T) {
t.Parallel()
// pluto_4.2.0_darwin_amd64.tar.gz = 9
// ruplacer-osx = 6
// croc_9.2.0_macOS-64bit.tar.gz = 7
// conftest_0.28.1_Darwin_x86_64.tar.gz = 7
// conftest_0.28.1_Darwin_arm64.tar.gz = 6
// pandoc-2.14.2-macOS.pkg = 6
// direnv.darwin-amd64 =8
osCap := &Capabilities{
OS: Darwin,
ExecutableMime: "application/x-mach-binary",
Arch: "arm64",
}
asset := Asset{
BrowserDownloadURL: "https://github.com/foo/bar/releases/download/v1.0/direnv.darwin-arm64",
}
require.Equal(t, 7, asset.EvaluateSuitability(osCap))
// pluto_4.2.0_darwin_amd64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/pluto_4.2.0_darwin_arm64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
// ruplacer-osx
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/ruplacer-osx"
require.Equal(t, 5, asset.EvaluateSuitability(osCap))
// croc_9.2.0_macOS-64bit.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/croc_9.2.0_macOS-64bit.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// conftest_0.28.1_Darwin_x86_64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/conftest_0.28.1_Darwin_x86_64.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// conftest_0.28.1_Darwin_arm64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/conftest_0.28.1_Darwin_arm64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
// pandoc-2.14.2-macOS.pkg
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/pandoc-2.14.2-macOS.pkg"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// gopass-1.15.11-darwin-amd64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/gopass-1.15.11-darwin-amd64.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// gopass-1.15.11-darwin-arm64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/gopass-1.15.11-darwin-arm64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
}
func TestEvalAssetSuitabilityLinux(t *testing.T) {
t.Parallel()
// pluto_4.2.0_darwin_amd64.tar.gz = 9
// ruplacer-osx = 6
// croc_9.2.0_macOS-64bit.tar.gz = 7
// conftest_0.28.1_Darwin_x86_64.tar.gz = 7
// conftest_0.28.1_Darwin_arm64.tar.gz = 6
// pandoc-2.14.2-macOS.pkg = 6
// direnv.darwin-amd64 =8
// helm-v4.0.4-linux-arm64.tar.gz.asc <0
osCap := &Capabilities{
OS: Linux,
ExecutableMime: "asdf",
Arch: "amd64",
}
asset := Asset{
BrowserDownloadURL: "https://github.com/foo/bar/releases/download/v1.0/direnv.linux-amd64",
}
require.Equal(t, 7, asset.EvaluateSuitability(osCap))
// pluto_4.2.0_darwin_amd64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/pluto_4.2.0_linux_amd64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
// ruplacer-osx
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/ruplacer-linux"
require.Equal(t, 5, asset.EvaluateSuitability(osCap))
// croc_9.2.0_macOS-64bit.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/croc_9.2.0_linuX-64bit.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// conftest_0.28.1_Darwin_x86_64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/conftest_0.28.1_Linux_x86_64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
// conftest_0.28.1_Darwin_arm64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/conftest_0.28.1_Linux_arm64.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// pandoc-2.14.2-macOS.pkg
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/pandoc-2.14.2-linux.pkg"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// gopass-1.15.11-darwin-amd64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/gopass-1.15.11-linux-arm64.tar.gz"
require.Equal(t, 6, asset.EvaluateSuitability(osCap))
// gopass-1.15.11-darwin-arm64.tar.gz
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/gopass-1.15.11-linux-amd64.tar.gz"
require.Equal(t, 9, asset.EvaluateSuitability(osCap))
// helm-v4.0.4-linux-arm64.tar.gz.asc
asset.BrowserDownloadURL = "https://github.com/foo/bar/releases/download/v1.0/helm-v4.0.4-linux-arm64.tar.gz.asc"
require.Less(t, asset.EvaluateSuitability(osCap), 0)
}
func TestFindGithubReleaseMacAssets(t *testing.T) {
t.Parallel()
var assets []Asset
asset1 := Asset{
BrowserDownloadURL: "https://github.com/trufflesecurity/trufflehog/releases/download/v3.60.1/trufflehog_3.60.1_linux_amd64.tar.gz",
}
asset2 := Asset{
BrowserDownloadURL: "https://github.com/trufflesecurity/trufflehog/releases/download/v3.60.1/trufflehog_3.60.1_linux_arm64.tar.gz",
}
assets = append(assets, asset1, asset2)
ghr := GithubRelease{
Assets: assets,
}
capAMD64 := &Capabilities{
OS: Linux,
Arch: "amd64",
}
capARM64 := &Capabilities{
OS: Linux,
Arch: "arm64",
}
downloadableAsset, _ := ghr.FindBestAsset(capAMD64)
require.Equal(t, asset1, *downloadableAsset)
downloadableAsset, _ = ghr.FindBestAsset(capARM64)
require.Equal(t, asset2, *downloadableAsset)
}
func TestGetHighestScore(t *testing.T) {
t.Parallel()
assetScores := map[int]int{}
assetScores[0] = 6
assetScores[1] = 8
assetScores[2] = 1
assetScores[3] = 9
assetScores[4] = 3
assetsByScore := getHighestScore(assetScores)
require.Equal(t, assetsByScore.Value, assetScores[3])
}
func TestInspectLinksInReleaseBody(t *testing.T) {
t.Parallel()
filename := filepath.Join("..", "..", "testdata", "helm-latest.json")
jsonBytes, err := os.ReadFile(filename)
if err != nil {
t.Fatal("error reading testdata: ", err)
}
ghr := GithubRelease{}
if err := json.Unmarshal(jsonBytes, &ghr); err != nil {
t.Fatal("error unmarshalling testdata: ", err)
}
asset, err := ghr.FindBestAsset(&Capabilities{
OS: Linux,
Arch: "amd64",
})
require.NoError(t, err)
require.Equal(t, "helm-v4.0.4-linux-amd64.tar.gz", asset.Name)
require.Contains(t, asset.BrowserDownloadURL, "get.helm.sh")
}
================================================
FILE: pkg/types/os.go
================================================
package types
import (
"runtime"
)
type OS int
const (
Darwin OS = iota
Linux
)
func IsDarwin() bool {
return runtime.GOOS == "darwin"
}
func IsLinux() bool {
return runtime.GOOS == "linux"
}
type Capabilities struct {
OS OS
ExecutableMime string
SharedLibrary string
Arch string
}
func GetOS() OS {
if runtime.GOOS == "darwin" {
return Darwin
}
if runtime.GOOS == "linux" {
return Linux
}
return -1
}
var current *Capabilities
func GetCapabilities() *Capabilities {
if current != nil {
return current
}
switch runtime.GOOS {
case "darwin":
current = &Capabilities{OS: Darwin, ExecutableMime: "application/x-mach-binary", SharedLibrary: "application/x-sharedlib"}
case "linux":
current = &Capabilities{OS: Linux, ExecutableMime: "application/x-executable", SharedLibrary: "application/x-sharedlib"}
}
current.Arch = runtime.GOARCH
return current
}
================================================
FILE: pkg/utils/utils.go
================================================
package utils
import (
"crhuber/kelp/pkg/logging"
"crhuber/kelp/pkg/types"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
)
func DirExists(dir string) bool {
info, err := os.Stat(dir)
if err != nil {
return false
}
return info.IsDir()
}
func FileExists(filename string) bool {
info, err := os.Stat(filename)
return !os.IsNotExist(err) && !info.IsDir()
}
func FilePathWalkDir(root string) ([]string, error) {
var files []string
err := filepath.Walk(root, func(path string, info os.FileInfo, _ error) error {
if !info.IsDir() {
files = append(files, path)
}
return nil
})
return files, err
}
func CopyFile(source, destination string) error {
from, err := os.Open(source)
if err != nil {
log.Fatal(err)
}
defer from.Close()
to, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE, 0744)
if err != nil {
return err
}
defer to.Close()
_, err = io.Copy(to, from)
if err != nil {
return err
}
return nil
}
func GetGithubRelease(owner, repo, release string) (*types.GithubRelease, error) {
var url string
if release == "latest" {
logging.LogInfo("🌐 Getting releases for %s/%s:%s...", owner, repo, release)
url = fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/%s", owner, repo, release)
} else {
// try by tag
logging.LogInfo("🌐 Getting releases by tag %s...", release)
url = fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", owner, repo, release)
}
// create client
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// set headers for github auth
ghToken := os.Getenv("GITHUB_TOKEN")
if ghToken != "" {
logging.LogDebug("Using Github token in http request")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ghToken))
}
// make request
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("invalid HTTP status: %v", resp.StatusCode)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
ghr := types.GithubRelease{}
if err := json.Unmarshal(body, &ghr); err != nil {
return nil, err
}
return &ghr, nil
}
================================================
FILE: pkg/utils/utils_test.go
================================================
package utils
import (
"os"
"path/filepath"
"testing"
"github.com/h2non/gock"
"github.com/stretchr/testify/require"
)
func TestGetGithubRelease(t *testing.T) {
defer gock.Off()
filename := filepath.Join("..", "..", "testdata", "helm-latest.json")
jsonBytes, err := os.ReadFile(filename)
if err != nil {
t.Fatal("error reading testdata: ", err)
}
gock.New("https://api.github.com").
Get("/repos/helm/helm/releases/latest").
Reply(200).
JSON(jsonBytes)
ghr, err := GetGithubRelease("helm", "helm", "latest")
require.NoError(t, err)
require.Equal(t, "v4.0.4", ghr.TagName)
}
================================================
FILE: testdata/helm-latest.json
================================================
{
"url": "https://api.github.com/repos/helm/helm/releases/270052681",
"assets_url": "https://api.github.com/repos/helm/helm/releases/270052681/assets",
"upload_url": "https://uploads.github.com/repos/helm/helm/releases/270052681/assets{?name,label}",
"html_url": "https://github.com/helm/helm/releases/tag/v4.0.4",
"id": 270052681,
"author": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"node_id": "RE_kwDOApspmc4QGK1J",
"tag_name": "v4.0.4",
"target_commitish": "main",
"name": "Helm v4.0.4",
"draft": false,
"immutable": false,
"prerelease": false,
"created_at": "2025-12-13T01:05:06Z",
"updated_at": "2025-12-13T01:23:13Z",
"published_at": "2025-12-13T01:22:50Z",
"assets": [
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071815",
"id": 328071815,
"node_id": "RA_kwDOApspmc4TjfqH",
"name": "helm-v4.0.4-darwin-amd64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:943463856d9a7da8abfdb6421e2b7bef3197565b32ecea04bfb0a93fa15db2fe",
"download_count": 266,
"created_at": "2025-12-13T01:23:08Z",
"updated_at": "2025-12-13T01:23:08Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-amd64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071822",
"id": 328071822,
"node_id": "RA_kwDOApspmc4TjfqO",
"name": "helm-v4.0.4-darwin-amd64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:5c0fcfab9868357ab5f540c6d470c3cf0c9373ab33d353f56914ba1731b656be",
"download_count": 170,
"created_at": "2025-12-13T01:23:08Z",
"updated_at": "2025-12-13T01:23:08Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-amd64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071823",
"id": 328071823,
"node_id": "RA_kwDOApspmc4TjfqP",
"name": "helm-v4.0.4-darwin-amd64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:1db897efd998fcadc5edd249a828b5d0401d6c2bd643828d9c528f607362e458",
"download_count": 172,
"created_at": "2025-12-13T01:23:08Z",
"updated_at": "2025-12-13T01:23:08Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-amd64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071827",
"id": 328071827,
"node_id": "RA_kwDOApspmc4TjfqT",
"name": "helm-v4.0.4-darwin-arm64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:fd44bbc3d2b875ad61713fd13de0dff34864d5c52381c7247bd1dfbebbc7316a",
"download_count": 187,
"created_at": "2025-12-13T01:23:08Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-arm64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071832",
"id": 328071832,
"node_id": "RA_kwDOApspmc4TjfqY",
"name": "helm-v4.0.4-darwin-arm64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:538510d78f375c6b2ef7d7b87ce33ee279680c32a670a33317c5c7a71228eee0",
"download_count": 147,
"created_at": "2025-12-13T01:23:08Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-arm64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071835",
"id": 328071835,
"node_id": "RA_kwDOApspmc4Tjfqb",
"name": "helm-v4.0.4-darwin-arm64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:6f6b2b3704ba05d7357563868335155f0d4b2852f430b57bd167486ee8b481b9",
"download_count": 153,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-darwin-arm64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071838",
"id": 328071838,
"node_id": "RA_kwDOApspmc4Tjfqe",
"name": "helm-v4.0.4-linux-386.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:02fa09a1e5f6f40b33e1fe7a8e404cced85f47480c3d272f4d75f297afe64b8f",
"download_count": 219,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-386.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071840",
"id": 328071840,
"node_id": "RA_kwDOApspmc4Tjfqg",
"name": "helm-v4.0.4-linux-386.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:ced78ed6756b7820b764b9487e7bee13d259ce8d943a1b6ad27f478b1bf019ed",
"download_count": 164,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-386.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071842",
"id": 328071842,
"node_id": "RA_kwDOApspmc4Tjfqi",
"name": "helm-v4.0.4-linux-386.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:b33b73be03e14589873a63b59a01ddd2f75d63d6783b206abcf1ca3a29f9359d",
"download_count": 166,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-386.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071844",
"id": 328071844,
"node_id": "RA_kwDOApspmc4Tjfqk",
"name": "helm-v4.0.4-linux-amd64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:37184ca689c09c8fbe67801b3c0c6da602013f84514f18a5da40351fb16e716d",
"download_count": 6591,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:09Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-amd64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071849",
"id": 328071849,
"node_id": "RA_kwDOApspmc4Tjfqp",
"name": "helm-v4.0.4-linux-amd64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:970ce8c0f959eb632867991e769ac0bcd8121bce15c4960f89d4cfe369268b03",
"download_count": 6101,
"created_at": "2025-12-13T01:23:09Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-amd64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071850",
"id": 328071850,
"node_id": "RA_kwDOApspmc4Tjfqq",
"name": "helm-v4.0.4-linux-amd64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:d3255c9bb0a7a12fa18998ba2b0bb0f1704c0fea07bf45c8a078274376bfebe0",
"download_count": 205,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-amd64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071851",
"id": 328071851,
"node_id": "RA_kwDOApspmc4Tjfqr",
"name": "helm-v4.0.4-linux-arm.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:3bfc27ba402781a4ebd20a2afddca7ec31f3ae0ef66317f769ae9c4840cd03cd",
"download_count": 146,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071853",
"id": 328071853,
"node_id": "RA_kwDOApspmc4Tjfqt",
"name": "helm-v4.0.4-linux-arm.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:b7d7ed2334381758e159154c649b4045bca08c8aff72caf22ced967e2e10746d",
"download_count": 137,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071856",
"id": 328071856,
"node_id": "RA_kwDOApspmc4Tjfqw",
"name": "helm-v4.0.4-linux-arm.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:98694fe87b905ca078bc844dd1e9b6ed8bf73e761fda33fcb11acfb0fdd4a56f",
"download_count": 139,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071858",
"id": 328071858,
"node_id": "RA_kwDOApspmc4Tjfqy",
"name": "helm-v4.0.4-linux-arm64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:29f58b92bc616ff27a96157c89f4c71b44ad74ff2b93225ef7e5bf6f04ca6444",
"download_count": 1328,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071859",
"id": 328071859,
"node_id": "RA_kwDOApspmc4Tjfqz",
"name": "helm-v4.0.4-linux-arm64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:85c2c1b29af5020aee6f865830ca7c78535582c08961e442c3841edfc4ae9bc6",
"download_count": 1305,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:10Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071868",
"id": 328071868,
"node_id": "RA_kwDOApspmc4Tjfq8",
"name": "helm-v4.0.4-linux-arm64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:7008fa1fe790ce223945651da31dec9e2147412ee6c77ffe9dfdb3a36e1fdde0",
"download_count": 133,
"created_at": "2025-12-13T01:23:10Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-arm64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071876",
"id": 328071876,
"node_id": "RA_kwDOApspmc4TjfrE",
"name": "helm-v4.0.4-linux-loong64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:6494e3aa743c4e7e18cc9d7f5afdadbc6798a5a0e8fc63dd2285b3a951982481",
"download_count": 143,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-loong64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071889",
"id": 328071889,
"node_id": "RA_kwDOApspmc4TjfrR",
"name": "helm-v4.0.4-linux-loong64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:8ae2f601edf1110027f54459dc40130a4395308bcdf75ec17a68f40b7706169a",
"download_count": 143,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-loong64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071891",
"id": 328071891,
"node_id": "RA_kwDOApspmc4TjfrT",
"name": "helm-v4.0.4-linux-loong64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:51fc79563532f1e3949bdf3b254f2d3c255dc8b79b399fd94262b675e97d20d8",
"download_count": 144,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-loong64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071892",
"id": 328071892,
"node_id": "RA_kwDOApspmc4TjfrU",
"name": "helm-v4.0.4-linux-ppc64le.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:61848dd0d2c20ad62145b89adc9ddcf7bb3d26bb486292662b6593a210645720",
"download_count": 51,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-ppc64le.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071894",
"id": 328071894,
"node_id": "RA_kwDOApspmc4TjfrW",
"name": "helm-v4.0.4-linux-ppc64le.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:7c5eb882502e657db2ea23d08974021f1d6585a2fd6acc8e7e2c9d2c65910f3a",
"download_count": 54,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-ppc64le.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071897",
"id": 328071897,
"node_id": "RA_kwDOApspmc4TjfrZ",
"name": "helm-v4.0.4-linux-ppc64le.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:ab546edb11ea05ad21b3a10ed94c289c8cd1e91be856914bcda68feb30a74bc5",
"download_count": 51,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:11Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-ppc64le.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071898",
"id": 328071898,
"node_id": "RA_kwDOApspmc4Tjfra",
"name": "helm-v4.0.4-linux-riscv64.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:91c1382737e7426f51c25e3643e24cd1e2c53a79d6564105b657dfc9ec33dcde",
"download_count": 142,
"created_at": "2025-12-13T01:23:11Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-riscv64.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071899",
"id": 328071899,
"node_id": "RA_kwDOApspmc4Tjfrb",
"name": "helm-v4.0.4-linux-riscv64.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:a9d3b44f24caafa628ace829f96043f13bebfe19635134bd3e81aa16a51809dc",
"download_count": 141,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-riscv64.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071900",
"id": 328071900,
"node_id": "RA_kwDOApspmc4Tjfrc",
"name": "helm-v4.0.4-linux-riscv64.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:f6ec68ffd30bf9b01083699097e67353eea81bba60164ba7c4d561f6bdce25e9",
"download_count": 144,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-riscv64.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071901",
"id": 328071901,
"node_id": "RA_kwDOApspmc4Tjfrd",
"name": "helm-v4.0.4-linux-s390x.tar.gz.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:699c5924c1ca2d02229e43f3f0120ad98a74007e52e9eae82cd54e0f30adb098",
"download_count": 52,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-s390x.tar.gz.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071902",
"id": 328071902,
"node_id": "RA_kwDOApspmc4Tjfre",
"name": "helm-v4.0.4-linux-s390x.tar.gz.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:72348b11c9e2c4868fd201a15657f4049f9c91c5cc51df5c0f2d7efb2eee7bd9",
"download_count": 49,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-s390x.tar.gz.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071903",
"id": 328071903,
"node_id": "RA_kwDOApspmc4Tjfrf",
"name": "helm-v4.0.4-linux-s390x.tar.gz.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:c6c42a06b3d02ee3b45f74ae7799dcddfdb3d626d8447f0bfa915ae96d5d8394",
"download_count": 51,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-linux-s390x.tar.gz.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071904",
"id": 328071904,
"node_id": "RA_kwDOApspmc4Tjfrg",
"name": "helm-v4.0.4-windows-amd64.zip.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:eb6d3871d204f77552c6e6783eec76b7182df3bd3ab60c4b95a34e6e73eba409",
"download_count": 340,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-amd64.zip.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071905",
"id": 328071905,
"node_id": "RA_kwDOApspmc4Tjfrh",
"name": "helm-v4.0.4-windows-amd64.zip.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:afd5bccf1a5ae673e7e765dc358e19f4a1ecc1a33b8bf1fedd8c04e085788779",
"download_count": 181,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:12Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-amd64.zip.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071906",
"id": 328071906,
"node_id": "RA_kwDOApspmc4Tjfri",
"name": "helm-v4.0.4-windows-amd64.zip.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:a813554ad03442e5b2ce6bd53ec6dd71c27b06f6bec449d1f5ebf2091d38db19",
"download_count": 161,
"created_at": "2025-12-13T01:23:12Z",
"updated_at": "2025-12-13T01:23:13Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-amd64.zip.sha256sum.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071907",
"id": 328071907,
"node_id": "RA_kwDOApspmc4Tjfrj",
"name": "helm-v4.0.4-windows-arm64.zip.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:63cfea9ba48829939f459ade799583247955116dd3bcaae7eecb4ca575aca933",
"download_count": 166,
"created_at": "2025-12-13T01:23:13Z",
"updated_at": "2025-12-13T01:23:13Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-arm64.zip.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071909",
"id": 328071909,
"node_id": "RA_kwDOApspmc4Tjfrl",
"name": "helm-v4.0.4-windows-arm64.zip.sha256.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:463780016e11b617f87a9c79b20513c61a0ddc9c27ebb42ee267c5a2e4654fed",
"download_count": 128,
"created_at": "2025-12-13T01:23:13Z",
"updated_at": "2025-12-13T01:23:13Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-arm64.zip.sha256.asc"
},
{
"url": "https://api.github.com/repos/helm/helm/releases/assets/328071910",
"id": 328071910,
"node_id": "RA_kwDOApspmc4Tjfrm",
"name": "helm-v4.0.4-windows-arm64.zip.sha256sum.asc",
"label": null,
"uploader": {
"login": "scottrigby",
"id": 407675,
"node_id": "MDQ6VXNlcjQwNzY3NQ==",
"avatar_url": "https://avatars.githubusercontent.com/u/407675?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/scottrigby",
"html_url": "https://github.com/scottrigby",
"followers_url": "https://api.github.com/users/scottrigby/followers",
"following_url": "https://api.github.com/users/scottrigby/following{/other_user}",
"gists_url": "https://api.github.com/users/scottrigby/gists{/gist_id}",
"starred_url": "https://api.github.com/users/scottrigby/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/scottrigby/subscriptions",
"organizations_url": "https://api.github.com/users/scottrigby/orgs",
"repos_url": "https://api.github.com/users/scottrigby/repos",
"events_url": "https://api.github.com/users/scottrigby/events{/privacy}",
"received_events_url": "https://api.github.com/users/scottrigby/received_events",
"type": "User",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/octet-stream",
"state": "uploaded",
"size": 833,
"digest": "sha256:4f27866327d78cca9bcafd836796bec8a0c79b15ac48a8001607593913a635ce",
"download_count": 129,
"created_at": "2025-12-13T01:23:13Z",
"updated_at": "2025-12-13T01:23:13Z",
"browser_download_url": "https://github.com/helm/helm/releases/download/v4.0.4/helm-v4.0.4-windows-arm64.zip.sha256sum.asc"
}
],
"tarball_url": "https://api.github.com/repos/helm/helm/tarball/v4.0.4",
"zipball_url": "https://api.github.com/repos/helm/helm/zipball/v4.0.4",
"body": "Helm v4.0.4 is a security fix for a Go CVE in the previous tag. This patch release rebuilds the Helm `v4.0.2` release with the latest Go toolchain, to fix the Go CVE. Users are encouraged to upgrade. Note that tag v4.0.3 was skipped due to a build failure.\r\n\r\nThe community keeps growing, and we'd love to see you there!\r\n\r\n- Join the discussion in [Kubernetes Slack](https://kubernetes.slack.com):\r\n - for questions and just to hang out\r\n - for discussing PRs, code, and bugs\r\n- Hang out at the Public Developer Call: Thursday, 9:30 Pacific via [Zoom](https://zoom.us/j/696660622)\r\n- Test, debug, and contribute charts: [ArtifactHub/packages](https://artifacthub.io/packages/search?kind=0)\r\n\r\n## Installation and Upgrading\r\n\r\nDownload Helm v4.0.4. The common platform binaries are here:\r\n\r\n- [MacOS amd64](https://get.helm.sh/helm-v4.0.4-darwin-amd64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-darwin-amd64.tar.gz.sha256sum) / 73bcfd6ab000fdc95acf9fe1c59e8e47179426a653e45ae485889869d4a00523)\r\n- [MacOS arm64](https://get.helm.sh/helm-v4.0.4-darwin-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-darwin-arm64.tar.gz.sha256sum) / a7ea99937a9679b3935fa0a2b70e577aa1ea84e5856e7c0821ca6ffa064ea976)\r\n- [Linux amd64](https://get.helm.sh/helm-v4.0.4-linux-amd64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-amd64.tar.gz.sha256sum) / 29454bc351f4433e66c00f5d37841627cbbcc02e4c70a6d796529d355237671c)\r\n- [Linux arm](https://get.helm.sh/helm-v4.0.4-linux-arm.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-arm.tar.gz.sha256sum) / 9255732e31b5aa5ee7b55be8497eea4723e3dfb08a63c37603ae0d15a9a9d82c)\r\n- [Linux arm64](https://get.helm.sh/helm-v4.0.4-linux-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-arm64.tar.gz.sha256sum) / 16b88acc6503d646b7537a298e7389bef469c5cc9ebadf727547abe9f6a35903)\r\n- [Linux i386](https://get.helm.sh/helm-v4.0.4-linux-386.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-386.tar.gz.sha256sum) / e6dbf45313bab48e51a2b7a5f3271a19bb3d8b9f07b4bb48ba342389d902af53)\r\n- [Linux loong64](https://get.helm.sh/helm-v4.0.4-linux-loong64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-loong64.tar.gz.sha256sum) / <?xml version=\"1.0\" encoding=\"utf-8\"?><Error><Code>BlobNotFound</Code><Message>The specified blob does not exist.\r\nRequestId:11673868-901e-003e-10cd-6b624b000000\r\nTime:2025-12-13T01:15:26.0922049Z</Message></Error>)\r\n- [Linux ppc64le](https://get.helm.sh/helm-v4.0.4-linux-ppc64le.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-ppc64le.tar.gz.sha256sum) / c108d181a0e29dadf281fbb4f4a0e0f2149922b119ec745ced1a5ae6f0918703)\r\n- [Linux s390x](https://get.helm.sh/helm-v4.0.4-linux-s390x.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-s390x.tar.gz.sha256sum) / cdf172c59379f0a3fe1db4743c16f122745fdaaebb2fbbfa40ce5722a4787717)\r\n- [Linux riscv64](https://get.helm.sh/helm-v4.0.4-linux-riscv64.tar.gz) ([checksum](https://get.helm.sh/helm-v4.0.4-linux-riscv64.tar.gz.sha256sum) / 2cf1c77d993bf5386e85249007bdaf38358d2516b18454212206a81b132e1330)\r\n- [Windows amd64](https://get.helm.sh/helm-v4.0.4-windows-amd64.zip) ([checksum](https://get.helm.sh/helm-v4.0.4-windows-amd64.zip.sha256sum) / 135bffadd3c87aff8856e06efb366bea2a48ac4d1742d73af80250410246f14d)\r\n- [Windows arm64](https://get.helm.sh/helm-v4.0.4-windows-arm64.zip) ([checksum](https://get.helm.sh/helm-v4.0.4-windows-arm64.zip.sha256sum) / b65d05f15260e78311f463773f54fe68f6d74444b3c3e84cecf270cdb927cd8a)\r\n\r\nThis release was signed with `208D D36E D5BB 3745 A167 43A4 C7C6 FBB5 B91C 1155` and can be found at @scottrigby [keybase account](https://keybase.io/r6by). Please use the attached signatures for verifying this release using `gpg`.\r\n\r\nThe [Quickstart Guide](https://helm.sh/docs/intro/quickstart/) will get you going from there. For **upgrade instructions** or detailed installation notes, check the [install guide](https://helm.sh/docs/intro/install/). You can also use a [script to install](https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3) on any system with `bash`.\r\n\r\n## What's Next\r\n\r\n- 3.19.5 and 4.0.5 are the next patch releases and will be on January 14, 2026\r\n- 3.20.0 and 4.1.0 is the next minor releases and will be on January 21, 2026\r\n\r\n## Changelog\r\n\r\n- Bump v4.0.2 CVE deps cd700e0627b8d9a4997a7ab2bc3b712d0de4dcd3 (George Jenkins)\r\n- Use latest patch release of Go in releases 9db13ee5c343196f642c568a03e58d3221b324d6 (Matt Farina)",
"reactions": {
"url": "https://api.github.com/repos/helm/helm/releases/270052681/reactions",
"total_count": 8,
"+1": 8,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"mentions_count": 1
}
gitextract_f2uvh5_n/
├── .github/
│ └── workflows/
│ ├── build.yaml
│ ├── codeql.yml
│ └── release.yaml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── .tool-versions
├── README.md
├── TODO.txt
├── Taskfile.yaml
├── go.mod
├── go.sum
├── main.go
├── pkg/
│ ├── config/
│ │ └── config.go
│ ├── install/
│ │ ├── install.go
│ │ └── install_test.go
│ ├── logging/
│ │ └── logging.go
│ ├── rm/
│ │ └── rm.go
│ ├── types/
│ │ ├── github.go
│ │ ├── github_test.go
│ │ └── os.go
│ └── utils/
│ ├── utils.go
│ └── utils_test.go
└── testdata/
└── helm-latest.json
SYMBOL INDEX (71 symbols across 11 files)
FILE: main.go
function main (line 24) | func main() {
FILE: pkg/config/config.go
type KelpConfig (line 27) | type KelpConfig struct
method Pop (line 40) | func (kc *KelpConfig) Pop(index int) []KelpPackage {
method GetPackage (line 44) | func (kc *KelpConfig) GetPackage(repo string) (*KelpPackage, error) {
method Save (line 76) | func (kc *KelpConfig) Save() error {
method RemovePackage (line 86) | func (kc *KelpConfig) RemovePackage(repo string) error {
method AddPackage (line 97) | func (kc *KelpConfig) AddPackage(owner, repo, release string) error {
method UpdatePackage (line 118) | func (kc *KelpConfig) UpdatePackage(repo string) (string, error) {
method SetPackage (line 131) | func (kc *KelpConfig) SetPackage(repo, release, description, binary st...
method List (line 151) | func (kc *KelpConfig) List() {
method Doctor (line 273) | func (kc *KelpConfig) Doctor() {
type KelpPackage (line 31) | type KelpPackage struct
function Load (line 65) | func Load(path string) (*KelpConfig, error) {
function Initialize (line 187) | func Initialize(path string) error {
function Inspect (line 240) | func Inspect() {
function Browse (line 255) | func Browse(owner, repo string) {
function commandExists (line 300) | func commandExists(cmd string) (string, error) {
FILE: pkg/install/install.go
function Install (line 25) | func Install(owner, repo, release string) error {
function unquarantineFile (line 60) | func unquarantineFile(filepath string) error {
function downloadFile (line 67) | func downloadFile(filepath string, url string) error {
function extractPackage (line 104) | func extractPackage(downloadPath, tempDir string) error {
function cleanBinaryName (line 153) | func cleanBinaryName(name string) string {
function extractFile (line 177) | func extractFile(f archives.FileInfo, destDir string) error {
function installBinary (line 204) | func installBinary(tempDir string) []string {
function copyToKelpBin (line 254) | func copyToKelpBin(file string) string {
function downloadGithubRelease (line 265) | func downloadGithubRelease(owner, repo, release string) (*types.Asset, e...
FILE: pkg/install/install_test.go
function TestCleanBinaryName (line 5) | func TestCleanBinaryName(t *testing.T) {
FILE: pkg/logging/logging.go
function SetLogVerbose (line 7) | func SetLogVerbose(verbose bool) {
function LogDebug (line 11) | func LogDebug(message string, args ...any) {
function LogInfo (line 17) | func LogInfo(message string, args ...any) {
function init (line 21) | func init() {
FILE: pkg/rm/rm.go
function RemoveBinary (line 11) | func RemoveBinary(binary string) error {
FILE: pkg/types/github.go
type Asset (line 13) | type Asset struct
method isDownloadableExtension (line 47) | func (a *Asset) isDownloadableExtension() bool {
method isChecksumFile (line 58) | func (a *Asset) isChecksumFile() bool {
method hasNoExtension (line 68) | func (a *Asset) hasNoExtension() bool {
method isMacAsset (line 75) | func (a *Asset) isMacAsset() bool {
method isLinuxAsset (line 87) | func (a *Asset) isLinuxAsset() bool {
method isSameOS (line 99) | func (a *Asset) isSameOS(capabilities *Capabilities) bool {
method isSameArchitecture (line 109) | func (a *Asset) isSameArchitecture(capabilities *Capabilities) bool {
method EvaluateSuitability (line 132) | func (a *Asset) EvaluateSuitability(capabilities *Capabilities) int {
method RealFilename (line 152) | func (a *Asset) RealFilename() string {
type GithubRelease (line 28) | type GithubRelease struct
method FindBestAsset (line 177) | func (ghr *GithubRelease) FindBestAsset(capabilities *Capabilities) (*...
method inspectLinksInReleaseBody (line 236) | func (ghr *GithubRelease) inspectLinksInReleaseBody() []string {
constant MIN_ASSET_SCORE (line 129) | MIN_ASSET_SCORE = 6
type Pair (line 165) | type Pair struct
type PairList (line 171) | type PairList
method Len (line 173) | func (p PairList) Len() int { return len(p) }
method Swap (line 174) | func (p PairList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
method Less (line 175) | func (p PairList) Less(i, j int) bool { return p[i].Value < p[j].Value }
function getHighestScore (line 223) | func getHighestScore(assetScores map[int]int) Pair {
FILE: pkg/types/github_test.go
function TestEvalAssetSuitabilityDarwin (line 12) | func TestEvalAssetSuitabilityDarwin(t *testing.T) {
function TestEvalAssetSuitabilityLinux (line 56) | func TestEvalAssetSuitabilityLinux(t *testing.T) {
function TestFindGithubReleaseMacAssets (line 104) | func TestFindGithubReleaseMacAssets(t *testing.T) {
function TestGetHighestScore (line 134) | func TestGetHighestScore(t *testing.T) {
function TestInspectLinksInReleaseBody (line 146) | func TestInspectLinksInReleaseBody(t *testing.T) {
FILE: pkg/types/os.go
type OS (line 7) | type OS
constant Darwin (line 10) | Darwin OS = iota
constant Linux (line 11) | Linux
function IsDarwin (line 14) | func IsDarwin() bool {
function IsLinux (line 18) | func IsLinux() bool {
type Capabilities (line 22) | type Capabilities struct
function GetOS (line 29) | func GetOS() OS {
function GetCapabilities (line 41) | func GetCapabilities() *Capabilities {
FILE: pkg/utils/utils.go
function DirExists (line 15) | func DirExists(dir string) bool {
function FileExists (line 23) | func FileExists(filename string) bool {
function FilePathWalkDir (line 28) | func FilePathWalkDir(root string) ([]string, error) {
function CopyFile (line 39) | func CopyFile(source, destination string) error {
function GetGithubRelease (line 59) | func GetGithubRelease(owner, repo, release string) (*types.GithubRelease...
FILE: pkg/utils/utils_test.go
function TestGetGithubRelease (line 12) | func TestGetGithubRelease(t *testing.T) {
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (182K chars).
[
{
"path": ".github/workflows/build.yaml",
"chars": 1160,
"preview": "name: Build and Test\npermissions:\n contents: read\n\non:\n pull_request:\n branches: [ master ]\n push:\n branches: ["
},
{
"path": ".github/workflows/codeql.yml",
"chars": 4688,
"preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
},
{
"path": ".github/workflows/release.yaml",
"chars": 619,
"preview": "name: goreleaser\n\non:\n push:\n tags:\n - '*'\n\njobs:\n goreleaser:\n runs-on: ubuntu-latest\n permissions:\n "
},
{
"path": ".gitignore",
"chars": 316,
"preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Ou"
},
{
"path": ".golangci.yml",
"chars": 879,
"preview": "version: \"2\"\nrun:\n go: \"1.23\"\nlinters:\n enable:\n - gocritic\n - gosec\n - misspell\n - revive\n disable:\n "
},
{
"path": ".goreleaser.yaml",
"chars": 1588,
"preview": "# GoReleaser configuration for kelp\nversion: 2\n\nproject_name: kelp\n\nbefore:\n hooks:\n - go mod tidy\n - go generate"
},
{
"path": ".tool-versions",
"chars": 14,
"preview": "golang 1.24.0\n"
},
{
"path": "README.md",
"chars": 3714,
"preview": "# KELP\n<p align=\"center\">\n <img height=\"150px\" src=\"./logo.png\" alt=\"KELP\" title=\"KELP\">\n</p>\n\n## What is it\nA simple "
},
{
"path": "TODO.txt",
"chars": 90,
"preview": "- when file already exists in cache do a hash before skipping it\n- add post install hooks\n"
},
{
"path": "Taskfile.yaml",
"chars": 3665,
"preview": "version: '3'\n\nvars:\n BINARY_NAME: kelp\n BUILD_DIR: ./build\n COVERAGE_DIR: ./coverage\n\ntasks:\n default:\n desc: Sho"
},
{
"path": "go.mod",
"chars": 1686,
"preview": "module crhuber/kelp\n\ngo 1.23.0\n\ntoolchain go1.23.8\n\nrequire (\n\tgithub.com/gabriel-vasile/mimetype v1.4.12\n\tgithub.com/h2"
},
{
"path": "go.sum",
"chars": 32347,
"preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1"
},
{
"path": "main.go",
"chars": 9915,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"crhuber/kelp/pkg/config\"\n\t\"crhuber/kelp/pkg/install\"\n\t\"crhuber/kelp/pkg/logging\"\n\t\"c"
},
{
"path": "pkg/config/config.go",
"chars": 6840,
"preview": "package config\n\nimport (\n\t\"crhuber/kelp/pkg/logging\"\n\t\"crhuber/kelp/pkg/types\"\n\t\"crhuber/kelp/pkg/utils\"\n\t\"encoding/json"
},
{
"path": "pkg/install/install.go",
"chars": 8148,
"preview": "package install\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strin"
},
{
"path": "pkg/install/install_test.go",
"chars": 900,
"preview": "package install\n\nimport \"testing\"\n\nfunc TestCleanBinaryName(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\twant "
},
{
"path": "pkg/logging/logging.go",
"chars": 405,
"preview": "package logging\n\nimport \"log\"\n\nvar logVerbose = false\n\nfunc SetLogVerbose(verbose bool) {\n\tlogVerbose = verbose\n}\n\nfunc "
},
{
"path": "pkg/rm/rm.go",
"chars": 356,
"preview": "package rm\n\nimport (\n\t\"crhuber/kelp/pkg/config\"\n\t\"crhuber/kelp/pkg/logging\"\n\t\"crhuber/kelp/pkg/utils\"\n\t\"os\"\n\t\"path/filep"
},
{
"path": "pkg/types/github.go",
"chars": 7615,
"preview": "package types\n\nimport (\n\t\"crhuber/kelp/pkg/logging\"\n\t\"errors\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Asset represent"
},
{
"path": "pkg/types/github_test.go",
"chars": 6471,
"preview": "package types\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfu"
},
{
"path": "pkg/types/os.go",
"chars": 913,
"preview": "package types\n\nimport (\n\t\"runtime\"\n)\n\ntype OS int\n\nconst (\n\tDarwin OS = iota\n\tLinux\n)\n\nfunc IsDarwin() bool {\n\treturn ru"
},
{
"path": "pkg/utils/utils.go",
"chars": 2227,
"preview": "package utils\n\nimport (\n\t\"crhuber/kelp/pkg/logging\"\n\t\"crhuber/kelp/pkg/types\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net"
},
{
"path": "pkg/utils/utils_test.go",
"chars": 598,
"preview": "package utils\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/h2non/gock\"\n\t\"github.com/stretchr/testify/requir"
},
{
"path": "testdata/helm-latest.json",
"chars": 72844,
"preview": "{\n \"url\": \"https://api.github.com/repos/helm/helm/releases/270052681\",\n \"assets_url\": \"https://api.github.com/repos/he"
}
]
About this extraction
This page contains the full source code of the crhuber/kelp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (164.1 KB), approximately 59.4k tokens, and a symbol index with 71 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.