├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── release.yaml ├── tui ├── model │ ├── browseModel.go │ ├── releasesModel.go │ ├── processingModel.go │ ├── detailModel.go │ ├── view.go │ ├── mainModel.go │ └── update.go ├── item │ ├── tool.go │ ├── browse.go │ └── release.go ├── styles │ └── styles.go ├── keys │ ├── processKeys.go │ ├── releasesKeys.go │ ├── keysbindigs.go │ └── detailKeys.go └── utils │ └── readme.go ├── Taskfile.yml ├── LICENSE ├── cli ├── update.go └── cli.go ├── .goreleaser.yml ├── main.go ├── go.mod ├── cliff.toml ├── tool ├── tools.go ├── eget.go └── tools.yaml ├── README.md ├── CHANGELOG.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | test/ 2 | notes.md 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /tui/model/browseModel.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/allaman/werkzeugkasten/tui/item" 5 | tea "github.com/charmbracelet/bubbletea" 6 | ) 7 | 8 | func openBrowserCmd(identifier string) tea.Cmd { 9 | return func() tea.Msg { 10 | err := item.OpenInBrowser(identifier) 11 | if err != nil { 12 | return browserErrMsg{err: err} 13 | } 14 | return browserSuccessMsg{} 15 | } 16 | } 17 | 18 | type browserSuccessMsg struct{} 19 | type browserErrMsg struct { 20 | err error 21 | } 22 | -------------------------------------------------------------------------------- /tui/item/tool.go: -------------------------------------------------------------------------------- 1 | package item 2 | 3 | type Tool struct { 4 | name string 5 | identifier string 6 | description string 7 | } 8 | 9 | func (i Tool) Title() string { return i.name } 10 | func (i Tool) Identifier() string { return i.identifier } 11 | func (i Tool) Description() string { return i.description } 12 | 13 | func NewItem(name, identifier, description string) Tool { 14 | return Tool{ 15 | name: name, 16 | description: description, 17 | identifier: identifier, 18 | } 19 | } 20 | 21 | func (i Tool) FilterValue() string { 22 | return i.name + " " + i.description 23 | } 24 | -------------------------------------------------------------------------------- /tui/styles/styles.go: -------------------------------------------------------------------------------- 1 | package styles 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | ) 6 | 7 | var ( 8 | TitleStyle = lipgloss.NewStyle(). 9 | Foreground(lipgloss.Color("#FAFAFA")). 10 | Background(lipgloss.Color("#7D56F4")). 11 | Padding(0, 1) 12 | 13 | InfoStyle = lipgloss.NewStyle(). 14 | Foreground(lipgloss.Color("#FAFAFA")). 15 | Background(lipgloss.Color("#2F2F2F")). 16 | Padding(0, 1) 17 | 18 | BorderStyle = lipgloss.NewStyle(). 19 | Border(lipgloss.RoundedBorder()). 20 | BorderForeground(lipgloss.Color("#874BFD")). 21 | Padding(1). 22 | MarginTop(1) 23 | ) 24 | -------------------------------------------------------------------------------- /tui/keys/processKeys.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import "github.com/charmbracelet/bubbles/key" 4 | 5 | type processKeyMap struct { 6 | Esc key.Binding 7 | Quit key.Binding 8 | } 9 | 10 | var ProcessingKeys = processKeyMap{ 11 | Esc: key.NewBinding( 12 | key.WithKeys("esc"), 13 | key.WithHelp("esc", "Esc"), 14 | ), 15 | Quit: key.NewBinding( 16 | key.WithKeys("q", "ctrl+c"), 17 | key.WithHelp("q", "quit"), 18 | ), 19 | } 20 | 21 | func (k processKeyMap) ShortHelp() []key.Binding { 22 | return []key.Binding{k.Esc, k.Quit} 23 | } 24 | 25 | func (k processKeyMap) FullHelp() [][]key.Binding { 26 | return [][]key.Binding{ 27 | {k.Esc, k.Quit}, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tui/keys/releasesKeys.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import "github.com/charmbracelet/bubbles/key" 4 | 5 | type releasesKeyMap struct { 6 | Install key.Binding 7 | Esc key.Binding 8 | } 9 | 10 | var ReleasesKeys = releasesKeyMap{ 11 | Install: key.NewBinding( 12 | key.WithKeys("i"), 13 | key.WithHelp("i", "install release"), 14 | ), 15 | Esc: key.NewBinding( 16 | key.WithKeys("esc"), 17 | key.WithHelp("esc", "Esc"), 18 | ), 19 | } 20 | 21 | func (k releasesKeyMap) ShortHelp() []key.Binding { 22 | return []key.Binding{k.Install, k.Esc} 23 | } 24 | 25 | func (k releasesKeyMap) FullHelp() [][]key.Binding { 26 | return [][]key.Binding{ 27 | {k.Install, k.Esc}, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tui/model/releasesModel.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "github.com/allaman/werkzeugkasten/tui/item" 8 | 9 | tea "github.com/charmbracelet/bubbletea" 10 | ) 11 | 12 | func fetchReleasesCmd(identifier string) tea.Cmd { 13 | return func() tea.Msg { 14 | token := os.Getenv("EGET_GITHUB_TOKEN") 15 | releases, err := item.FetchReleases(identifier, token) 16 | if err != nil { 17 | slog.Debug("error fetching releases", "error", err) 18 | return fetchReleasesErrMsg{err: err} 19 | } 20 | return fetchReleasesSuccessMsg(releases) 21 | } 22 | } 23 | 24 | type fetchReleasesSuccessMsg []item.FetchRelease 25 | type fetchReleasesErrMsg struct { 26 | err error 27 | } 28 | -------------------------------------------------------------------------------- /tui/model/processingModel.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/allaman/werkzeugkasten/tool" 5 | 6 | tea "github.com/charmbracelet/bubbletea" 7 | ) 8 | 9 | func (m *MainModel) processSelectedItem() tea.Cmd { 10 | return func() tea.Msg { 11 | tool.InstallEget(m.config.DownloadDir) 12 | item := m.ToolData.Tools[m.ProcessingModel.ItemName] 13 | if m.ProcessingModel.ItemTag != "" { 14 | item.Tag = m.ProcessingModel.ItemTag 15 | } 16 | err := tool.DownloadToolWithEget(m.config.DownloadDir, item) 17 | if err != nil { 18 | return processErrMsg{err: err} 19 | } 20 | return processSuccessMsg("Install complete.\n") 21 | } 22 | } 23 | 24 | type processSuccessMsg string 25 | type processErrMsg struct { 26 | err error 27 | } 28 | -------------------------------------------------------------------------------- /tui/model/detailModel.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/allaman/werkzeugkasten/tui/utils" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | 10 | "github.com/charmbracelet/glamour" 11 | ) 12 | 13 | func fetchReadmeCmd(url string) tea.Cmd { 14 | return func() tea.Msg { 15 | content, err := utils.FetchReadme(url) 16 | if err != nil { 17 | slog.Debug("error fetching README", "error", err) 18 | return fetchReadmeErrMsg{err: err} 19 | } 20 | 21 | renderedContent, err := glamour.Render(content, "dark") 22 | if err != nil { 23 | slog.Debug("error rendering content", "error", err) 24 | return fetchReadmeErrMsg{err: err} 25 | } 26 | 27 | return fetchReadmeSuccessMsg(renderedContent) 28 | } 29 | } 30 | 31 | type fetchReadmeSuccessMsg string 32 | type fetchReadmeErrMsg struct { 33 | err error 34 | } 35 | -------------------------------------------------------------------------------- /tui/model/view.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/allaman/werkzeugkasten/tui/keys" 7 | ) 8 | 9 | func (m *MainModel) View() string { 10 | switch m.CurrentView { 11 | case "tools": 12 | return m.ToolsListView.View() 13 | case "detail": 14 | helpView := m.DetailView.Help.View(keys.DetailKeys) 15 | return fmt.Sprintf("%s\n%s\n%s\n%s", m.headerView(), m.DetailView.ViewPort.View(), m.footerView(), helpView) 16 | case "releases": 17 | return m.ReleasesListView.View() 18 | case "processing": 19 | helpView := m.ProcessingModel.Help.View(keys.ProcessingKeys) 20 | return fmt.Sprintf("%s\n%s\n%s\n%s", m.headerView(), m.ProcessingModel.ViewPort.View(), m.footerView(), helpView) 21 | case "version": 22 | return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.showVersion(), m.footerView()) 23 | default: 24 | return "Unknown view" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | # Only release when a new GH tag is added 5 | on: 6 | push: 7 | tags: 8 | - "[0-9]+.[0-9]+.[0-9]+" 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | # Checkout and set env 18 | - name: Checkout 19 | uses: actions/checkout@v6 20 | # It is required for the changelog to work correctly. 21 | with: 22 | fetch-depth: 0 23 | - name: Set up Go 24 | uses: actions/setup-go@v6 25 | with: 26 | go-version: 1.24 27 | # Build & Release binaries 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v6 30 | if: success() && startsWith(github.ref, 'refs/tags/') 31 | with: 32 | version: latest 33 | args: release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 36 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3" 3 | 4 | tasks: 5 | default: 6 | silent: true 7 | cmds: 8 | - task -l 9 | 10 | install: 11 | desc: Install App 12 | cmds: 13 | - task: fmt 14 | - task: lint 15 | - task: vet 16 | - task: build 17 | - go install -ldflags "-s -w -X github.com/allaman/werkzeugkasten/cli.Version=0.1.0" 18 | 19 | build: 20 | desc: Build App 21 | cmds: 22 | - go build 23 | 24 | run: 25 | desc: Run App 26 | cmds: 27 | - go build && ./werkzeugkasten 28 | 29 | lint: 30 | desc: Run linter 31 | cmds: 32 | - golangci-lint run . 33 | 34 | fmt: 35 | desc: Run formatter 36 | cmds: 37 | - go fmt . 38 | 39 | upgrade-deps: 40 | desc: Upgrade all dependencies 41 | cmds: 42 | - go get -u ./... 43 | 44 | vet: 45 | desc: Run go vet 46 | cmds: 47 | - go vet 48 | 49 | vuln: 50 | desc: Check for vulnerabilities 51 | cmds: 52 | - govulncheck . 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Michael Peter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli/update.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "runtime" 9 | 10 | "github.com/creativeprojects/go-selfupdate" 11 | ) 12 | 13 | func Update(currentVersion string) error { 14 | latest, found, err := selfupdate.DetectLatest(context.Background(), selfupdate.ParseSlug("allaman/werkzeugkasten")) 15 | if err != nil { 16 | return fmt.Errorf("error occurred while detecting version: %w", err) 17 | } 18 | if !found { 19 | return fmt.Errorf("latest version for %s/%s could not be found from github repository", runtime.GOOS, runtime.GOARCH) 20 | } 21 | 22 | if latest.LessOrEqual(currentVersion) { 23 | slog.Info("current version is the latest version", "version", currentVersion) 24 | return nil 25 | } 26 | 27 | exe, err := selfupdate.ExecutablePath() 28 | if err != nil { 29 | return errors.New("could not locate executable path") 30 | } 31 | if err := selfupdate.UpdateTo(context.Background(), latest.AssetURL, latest.AssetName, exe); err != nil { 32 | return fmt.Errorf("error occurred while updating binary: %w", err) 33 | } 34 | slog.Info(fmt.Sprintf("Successfully updated to version %s", latest.Version())) 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /tui/keys/keysbindigs.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import "github.com/charmbracelet/bubbles/key" 4 | 5 | type KeyMap struct { 6 | Install key.Binding 7 | Releases key.Binding 8 | Describe key.Binding 9 | Browse key.Binding 10 | Quit key.Binding 11 | Esc key.Binding 12 | Version key.Binding 13 | } 14 | 15 | var ToolsKeys = KeyMap{ 16 | Install: key.NewBinding( 17 | key.WithKeys("i"), 18 | key.WithHelp("i", "install"), 19 | ), 20 | Describe: key.NewBinding( 21 | key.WithKeys("d"), 22 | key.WithHelp("d", "describe"), 23 | ), 24 | Releases: key.NewBinding( 25 | key.WithKeys("r"), 26 | key.WithHelp("r", "releases"), 27 | ), 28 | Browse: key.NewBinding( 29 | key.WithKeys("b"), 30 | key.WithHelp("b", "browse"), 31 | ), 32 | Quit: key.NewBinding( 33 | key.WithKeys("q", "ctrl+c"), 34 | key.WithHelp("q", "quit"), 35 | ), 36 | Esc: key.NewBinding( 37 | key.WithKeys("esc"), 38 | key.WithHelp("esc", "Esc"), 39 | ), 40 | Version: key.NewBinding( 41 | key.WithKeys("v"), 42 | key.WithHelp("v", "version"), 43 | ), 44 | } 45 | 46 | func (k KeyMap) ShortHelp() []key.Binding { 47 | return []key.Binding{k.Install} 48 | } 49 | 50 | func (k KeyMap) FullHelp() []key.Binding { 51 | return []key.Binding{k.Install, k.Describe, k.Releases, k.Browse, k.Esc, k.Version} 52 | } 53 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | project_name: werkzeugkasten 4 | env: 5 | - CGO_ENABLED=0 6 | builds: 7 | - binary: werkzeugkasten 8 | id: werkzeugkasten 9 | main: ./ 10 | ldflags: 11 | - -s -w -X github.com/allaman/werkzeugkasten/cli.Version={{ .Version }} 12 | targets: 13 | - darwin_amd64 14 | - darwin_arm64 15 | - linux_amd64 16 | - linux_arm64 17 | archives: 18 | - formats: binary 19 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ if eq .Arch `amd64` }}x86_64{{ else }}{{ .Arch }}{{ end }}" 20 | checksum: 21 | algorithm: sha256 22 | changelog: 23 | sort: asc 24 | use: github 25 | groups: 26 | - title: Dependency updates 27 | regexp: '^.*?(feat|fix|chore)\(deps\)!?:.+$' 28 | order: 300 29 | - title: "New Features" 30 | regexp: '^.*?feat(\(.+\))??!?:.+$' 31 | order: 100 32 | - title: "Bug fixes" 33 | regexp: '^.*?(fix|refactor)(\(.+\))??!?:.+$' 34 | order: 200 35 | - title: "Documentation updates" 36 | regexp: ^.*?docs?(\(.+\))??!?:.+$ 37 | order: 400 38 | - title: "Build process updates" 39 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ 40 | order: 400 41 | - title: Other work 42 | order: 9999 43 | release: 44 | name_template: "{{ .Version }}" 45 | footer: | 46 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/{{ .PreviousTag }}...{{ .Tag }} 47 | -------------------------------------------------------------------------------- /tui/utils/readme.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func FetchReadme(url string) (string, error) { 13 | client := http.Client{ 14 | Timeout: time.Second * 5, 15 | } 16 | var resp *http.Response 17 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) 18 | if err != nil { 19 | return "", err 20 | } 21 | resp, err = client.Do(req) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | // we don't know the default branch so we also fetch from "master" 27 | if resp.StatusCode == http.StatusNotFound { 28 | newURL := strings.Replace(url, "/main/", "/master/", 1) 29 | req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, newURL, nil) 30 | if err != nil { 31 | return "", err 32 | } 33 | resp, err = client.Do(req) 34 | if err != nil { 35 | return "", err 36 | } 37 | } 38 | 39 | if resp.StatusCode != http.StatusOK { 40 | return "", fmt.Errorf("status code for downloading README was not ok: %d", resp.StatusCode) 41 | } 42 | 43 | defer func() { 44 | if closeErr := resp.Body.Close(); closeErr != nil { 45 | if err == nil { 46 | err = fmt.Errorf("error closing response body: %w", closeErr) 47 | } 48 | } 49 | }() 50 | 51 | body, err := io.ReadAll(resp.Body) 52 | if err != nil { 53 | return "", err 54 | } 55 | 56 | return string(body), nil 57 | } 58 | -------------------------------------------------------------------------------- /tui/keys/detailKeys.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import "github.com/charmbracelet/bubbles/key" 4 | 5 | type detailsKeyMap struct { 6 | Down key.Binding 7 | Up key.Binding 8 | HalfPageDown key.Binding 9 | HalfPageUp key.Binding 10 | Help key.Binding 11 | Install key.Binding 12 | Esc key.Binding 13 | Quit key.Binding 14 | } 15 | 16 | var DetailKeys = detailsKeyMap{ 17 | Quit: key.NewBinding( 18 | key.WithKeys("q", "ctrl+c"), 19 | key.WithHelp("q", "quit"), 20 | ), 21 | Down: key.NewBinding( 22 | key.WithKeys("down", "j"), 23 | key.WithHelp("↓/j", "scroll down"), 24 | ), 25 | Up: key.NewBinding( 26 | key.WithKeys("up", "k"), 27 | key.WithHelp("↑/k", "scroll up"), 28 | ), 29 | HalfPageDown: key.NewBinding( 30 | key.WithKeys("pgdown"), 31 | key.WithHelp("pgdn", "page down"), 32 | ), 33 | HalfPageUp: key.NewBinding( 34 | key.WithKeys("pgup"), 35 | key.WithHelp("pgup", "page up"), 36 | ), 37 | Install: key.NewBinding( 38 | key.WithKeys("i"), 39 | key.WithHelp("i", "install"), 40 | ), 41 | Esc: key.NewBinding( 42 | key.WithKeys("esc"), 43 | key.WithHelp("esc", "Esc"), 44 | ), 45 | Help: key.NewBinding( 46 | key.WithKeys("?"), 47 | key.WithHelp("?", "toggle help"), 48 | ), 49 | } 50 | 51 | func (k detailsKeyMap) ShortHelp() []key.Binding { 52 | return []key.Binding{k.Down, k.Up, k.Install, k.Help, k.Esc, k.Quit} 53 | } 54 | 55 | func (k detailsKeyMap) FullHelp() [][]key.Binding { 56 | return [][]key.Binding{ 57 | {k.Down, k.Up, k.Esc, k.Help, k.Quit}, 58 | {k.HalfPageUp, k.HalfPageDown}, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tui/item/browse.go: -------------------------------------------------------------------------------- 1 | package item 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | // openInBrowser opens the given tool identifier in the system's default browser 11 | func OpenInBrowser(identifier string) error { 12 | url := constructURL(identifier) 13 | if url == "" { 14 | return fmt.Errorf("unsupported identifier format: %s", identifier) 15 | } 16 | return openURL(url) 17 | } 18 | 19 | // constructURL converts a tool identifier to a proper URL 20 | func constructURL(identifier string) string { 21 | if strings.HasPrefix(identifier, "https://") || strings.HasPrefix(identifier, "http://") { 22 | return identifier 23 | } 24 | if isGitHubRepo(identifier) { 25 | return fmt.Sprintf("https://github.com/%s", identifier) 26 | } 27 | return "" 28 | } 29 | 30 | // isGitHubRepo checks if the identifier looks like a GitHub repository identifier (owner/repo) 31 | func isGitHubRepo(identifier string) bool { 32 | parts := strings.Split(identifier, "/") 33 | if len(parts) != 2 { 34 | return false 35 | } 36 | for _, part := range parts { 37 | if part == "" || strings.Contains(part, " ") { 38 | return false 39 | } 40 | } 41 | return true 42 | } 43 | 44 | // openURL opens the given URL using the system's default browser 45 | func openURL(url string) error { 46 | var cmd string 47 | var args []string 48 | 49 | switch runtime.GOOS { 50 | case "darwin": 51 | cmd = "open" 52 | case "linux": 53 | cmd = "xdg-open" 54 | case "windows": 55 | cmd = "start" 56 | args = append(args, "") 57 | default: 58 | return fmt.Errorf("unsupported platform: %s", runtime.GOOS) 59 | } 60 | 61 | if _, err := exec.LookPath(cmd); err != nil { 62 | return fmt.Errorf("no browser opener found (%s): %v", cmd, err) 63 | } 64 | 65 | args = append(args, url) 66 | return exec.Command(cmd, args...).Start() 67 | } 68 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "time" 8 | 9 | "github.com/allaman/werkzeugkasten/cli" 10 | "github.com/allaman/werkzeugkasten/tool" 11 | "github.com/allaman/werkzeugkasten/tui/model" 12 | 13 | tea "github.com/charmbracelet/bubbletea" 14 | ) 15 | 16 | func main() { 17 | cfg := cli.Cli() 18 | if cfg.Debug { 19 | opts := &slog.HandlerOptions{ 20 | Level: slog.LevelDebug, 21 | AddSource: true, 22 | ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { 23 | if a.Key == slog.TimeKey { 24 | if t, ok := a.Value.Any().(time.Time); ok { 25 | return slog.String(a.Key, t.Format("2006-01-02 15:04:05")) 26 | } 27 | } 28 | return a 29 | }, 30 | } 31 | logger := slog.New(slog.NewTextHandler(os.Stdout, opts)) 32 | slog.SetDefault(logger) 33 | } 34 | tools, err := tool.CreateToolData() 35 | if err != nil { 36 | slog.Error("could not parse tools data", "error", err) 37 | os.Exit(1) 38 | } 39 | slog.Debug("download dir", "dir", cfg.DownloadDir) 40 | 41 | if cfg.Tools { 42 | tool.PrintTools(tools) 43 | slog.Debug("found tools", "count", len(tools.Tools)) 44 | os.Exit(0) 45 | } 46 | if cfg.Categories { 47 | tool.PrintCategories(tool.GetCategories(tools)) 48 | os.Exit(0) 49 | } 50 | if cfg.Category != "" { 51 | result := tool.GetToolsByCategory(cfg.Category, tools) 52 | if len(result.Tools) == 0 { 53 | slog.Warn("no results found", "category", cfg.Category) 54 | os.Exit(0) 55 | } 56 | tool.PrintTools(result) 57 | slog.Debug("found tools", "category", cfg.Category, "count", len(result.Tools)) 58 | os.Exit(0) 59 | } 60 | // interactive mode 61 | if len(cfg.ToolList) == 0 { 62 | p := tea.NewProgram(model.InitialModel(tools, cfg), tea.WithAltScreen()) 63 | if _, err := p.Run(); err != nil { 64 | fmt.Printf("Error: %v", err) 65 | os.Exit(1) 66 | } 67 | } else { 68 | // non-interactive mode 69 | tool.InstallEget(cfg.DownloadDir) 70 | for _, toolName := range cfg.ToolList { 71 | err = tool.DownloadToolWithEget(cfg.DownloadDir, tools.Tools[toolName]) 72 | if err != nil { 73 | slog.Warn("could not download tool", "tool", toolName, "error", err) 74 | continue 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // will be overwritten in release pipeline 12 | var Version = "dev" 13 | 14 | type CliConfig struct { 15 | Category string 16 | Debug bool 17 | DownloadDir string 18 | Tools bool 19 | Categories bool 20 | ToolList toolList 21 | } 22 | type toolList []string 23 | 24 | func (s *toolList) String() string { 25 | return strings.Join(*s, ", ") 26 | } 27 | 28 | func (s *toolList) Set(value string) error { 29 | *s = append(*s, value) 30 | return nil 31 | } 32 | 33 | func Cli() CliConfig { 34 | var cliFlags CliConfig 35 | var toolList toolList 36 | helpFlag := flag.Bool("help", false, "Print help message") 37 | versionFlag := flag.Bool("version", false, "Print version") 38 | updateFlag := flag.Bool("update", false, "Self-update") 39 | debugFlag := flag.Bool("debug", false, "Enable debug output") 40 | downloadDirFlag := flag.String("dir", ".", "Where to download the tools") 41 | listToolsFlag := flag.Bool("tools", false, "Print all available tools") 42 | listCategoriesFlag := flag.Bool("categories", false, "Print all categories and tool count") 43 | listByCategoriesFlag := flag.String("category", "", "List tools by category") 44 | flag.Var(&toolList, "tool", "Specify multiple tools to install programmatically (e.g., -tool kustomize -tool task)") 45 | flag.Parse() 46 | if *helpFlag { 47 | fmt.Println("Usage: werkzeugkasten [flags]") 48 | fmt.Println("Flags:") 49 | flag.PrintDefaults() 50 | os.Exit(0) 51 | } 52 | if *versionFlag { 53 | fmt.Println(Version) 54 | os.Exit(0) 55 | } 56 | if *updateFlag { 57 | if err := Update(Version); err != nil { 58 | slog.Error("could not self-update", "err", err) 59 | os.Exit(1) 60 | } 61 | os.Exit(0) 62 | } 63 | if *listToolsFlag { 64 | cliFlags.Tools = true 65 | } 66 | if *listCategoriesFlag { 67 | cliFlags.Categories = true 68 | } 69 | if *debugFlag { 70 | cliFlags.Debug = true 71 | } 72 | if *downloadDirFlag != "" { 73 | cliFlags.DownloadDir = *downloadDirFlag 74 | } 75 | if *listByCategoriesFlag != "" { 76 | cliFlags.Category = *listByCategoriesFlag 77 | } 78 | cliFlags.ToolList = []string{} 79 | if len(toolList) > 0 { 80 | cliFlags.ToolList = toolList 81 | } 82 | return cliFlags 83 | } 84 | -------------------------------------------------------------------------------- /tui/item/release.go: -------------------------------------------------------------------------------- 1 | package item 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | type Release struct { 12 | Tag string 13 | PublishedAt time.Time 14 | } 15 | 16 | // Mandatory for list.Model 17 | func (r Release) Title() string { return r.Tag } 18 | func (r Release) Description() string { return r.PublishedAt.Format("2006-01-02") } 19 | 20 | func NewRelease(tagName string, publishedAt time.Time) Release { 21 | return Release{ 22 | Tag: tagName, 23 | PublishedAt: publishedAt, 24 | } 25 | } 26 | 27 | func (r Release) FilterValue() string { 28 | return r.Tag 29 | } 30 | 31 | type FetchRelease struct { 32 | Name string `json:"name"` 33 | TagName string `json:"tag_name"` 34 | ID int64 `json:"id"` 35 | CreatedAt time.Time `json:"created_at"` 36 | PublishedAt time.Time `json:"published_at"` 37 | URL string `json:"url"` 38 | } 39 | 40 | // FetchReleases fetches releases for a GitHub repository 41 | // If token is an empty string, the request will be made without authentication 42 | func FetchReleases(identifier, token string) ([]FetchRelease, error) { 43 | // NOTE: 100 is max per_page. For more, pagination is needed. 44 | url := fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=100", identifier) 45 | 46 | client := &http.Client{ 47 | Timeout: 10 * time.Second, 48 | } 49 | 50 | req, err := http.NewRequest("GET", url, nil) 51 | if err != nil { 52 | return nil, fmt.Errorf("failed to create request: %w", err) 53 | } 54 | 55 | req.Header.Add("Accept", "application/vnd.github+json") 56 | req.Header.Add("X-GitHub-Api-Version", "2022-11-28") 57 | 58 | // Add authorization header only if token is provided 59 | if token != "" { 60 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) 61 | } 62 | 63 | resp, err := client.Do(req) 64 | if err != nil { 65 | return nil, fmt.Errorf("request failed: %w", err) 66 | } 67 | defer func() { 68 | if closeErr := resp.Body.Close(); closeErr != nil { 69 | if err == nil { 70 | err = fmt.Errorf("error closing response body: %w", closeErr) 71 | } 72 | } 73 | }() 74 | 75 | if resp.StatusCode != http.StatusOK { 76 | body, _ := io.ReadAll(resp.Body) 77 | return nil, fmt.Errorf("GitHub API returned non-200 status: %d - %s", resp.StatusCode, string(body)) 78 | } 79 | 80 | var releases []FetchRelease 81 | if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { 82 | return nil, fmt.Errorf("failed to decode response: %w", err) 83 | } 84 | 85 | return releases, nil 86 | } 87 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/allaman/werkzeugkasten 2 | 3 | go 1.24.6 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.21.0 7 | github.com/charmbracelet/bubbletea v1.3.10 8 | github.com/charmbracelet/glamour v0.10.0 9 | github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 10 | github.com/creativeprojects/go-selfupdate v1.5.1 11 | github.com/goccy/go-yaml v1.18.0 12 | ) 13 | 14 | require ( 15 | code.gitea.io/sdk/gitea v0.22.0 // indirect 16 | github.com/42wim/httpsig v1.2.3 // indirect 17 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 18 | github.com/alecthomas/chroma/v2 v2.20.0 // indirect 19 | github.com/atotto/clipboard v0.1.4 // indirect 20 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 21 | github.com/aymerick/douceur v0.2.0 // indirect 22 | github.com/charmbracelet/colorprofile v0.3.2 // indirect 23 | github.com/charmbracelet/x/ansi v0.10.1 // indirect 24 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 25 | github.com/charmbracelet/x/exp/slice v0.0.0-20250919153222-1038f7e6fef4 // indirect 26 | github.com/charmbracelet/x/term v0.2.1 // indirect 27 | github.com/davidmz/go-pageant v1.0.2 // indirect 28 | github.com/dlclark/regexp2 v1.11.5 // indirect 29 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 30 | github.com/go-fed/httpsig v1.1.0 // indirect 31 | github.com/google/go-github/v30 v30.1.0 // indirect 32 | github.com/google/go-querystring v1.1.0 // indirect 33 | github.com/gorilla/css v1.0.1 // indirect 34 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 35 | github.com/hashicorp/go-retryablehttp v0.7.8 // indirect 36 | github.com/hashicorp/go-version v1.7.0 // indirect 37 | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect 38 | github.com/mattn/go-isatty v0.0.20 // indirect 39 | github.com/mattn/go-localereader v0.0.1 // indirect 40 | github.com/mattn/go-runewidth v0.0.16 // indirect 41 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect 42 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 43 | github.com/muesli/cancelreader v0.2.2 // indirect 44 | github.com/muesli/reflow v0.3.0 // indirect 45 | github.com/muesli/termenv v0.16.0 // indirect 46 | github.com/rivo/uniseg v0.4.7 // indirect 47 | github.com/sahilm/fuzzy v0.1.1 // indirect 48 | github.com/ulikunitz/xz v0.5.15 // indirect 49 | github.com/xanzy/go-gitlab v0.115.0 // indirect 50 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 51 | github.com/yuin/goldmark v1.7.13 // indirect 52 | github.com/yuin/goldmark-emoji v1.0.6 // indirect 53 | golang.org/x/crypto v0.42.0 // indirect 54 | golang.org/x/net v0.44.0 // indirect 55 | golang.org/x/oauth2 v0.31.0 // indirect 56 | golang.org/x/sync v0.17.0 // indirect 57 | golang.org/x/sys v0.36.0 // indirect 58 | golang.org/x/term v0.35.0 // indirect 59 | golang.org/x/text v0.29.0 // indirect 60 | golang.org/x/time v0.13.0 // indirect 61 | gopkg.in/yaml.v3 v3.0.1 // indirect 62 | ) 63 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ configuration file 2 | # https://git-cliff.org/docs/configuration 3 | 4 | [remote.github] 5 | owner = "allaman" 6 | repo = "werkzeugkasten" 7 | 8 | [changelog] 9 | # template for the changelog body 10 | # https://keats.github.io/tera/docs/#introduction 11 | body = """ 12 | ## What's Changed 13 | 14 | {%- if version %} in {{ version }}{%- endif -%} 15 | {% for commit in commits %} 16 | {% if commit.remote.pr_title -%} 17 | {%- set commit_message = commit.remote.pr_title -%} 18 | {%- else -%} 19 | {%- set commit_message = commit.message -%} 20 | {%- endif -%} 21 | * {{ commit_message | split(pat="\n") | first | trim }}\ 22 | {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%} 23 | {% if commit.remote.pr_number %} in \ 24 | [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \ 25 | {%- endif %} 26 | {%- endfor -%} 27 | 28 | {%- if github -%} 29 | {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} 30 | {% raw %}\n{% endraw -%} 31 | ## New Contributors 32 | {%- endif %}\ 33 | {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} 34 | * @{{ contributor.username }} made their first contribution 35 | {%- if contributor.pr_number %} in \ 36 | [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ 37 | {%- endif %} 38 | {%- endfor -%} 39 | {%- endif -%} 40 | 41 | {% if version %} 42 | {% if previous.version %} 43 | **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }} 44 | {% endif %} 45 | {% else -%} 46 | {% raw %}\n{% endraw %} 47 | {% endif %} 48 | 49 | {%- macro remote_url() -%} 50 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 51 | {%- endmacro -%} 52 | """ 53 | # remove the leading and trailing whitespace from the template 54 | trim = true 55 | # changelog footer 56 | footer = """ 57 | """ 58 | # postprocessors 59 | postprocessors = [] 60 | 61 | [git] 62 | # parse the commits based on https://www.conventionalcommits.org 63 | conventional_commits = false 64 | # filter out the commits that are not conventional 65 | filter_unconventional = true 66 | # process each line of a commit as an individual commit 67 | split_commits = false 68 | # regex for preprocessing the commit messages 69 | commit_preprocessors = [ 70 | # remove issue numbers from commits 71 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" }, 72 | ] 73 | # protect breaking changes from being skipped due to matching a skipping commit_parser 74 | protect_breaking_commits = false 75 | # filter out the commits that are not matched by commit parsers 76 | filter_commits = false 77 | # regex for matching git tags 78 | tag_pattern = "[0-9].*" 79 | # sort the tags topologically 80 | topo_order = false 81 | # sort the commits inside sections by oldest/newest order 82 | sort_commits = "newest" 83 | -------------------------------------------------------------------------------- /tui/model/mainModel.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/allaman/werkzeugkasten/cli" 8 | "github.com/allaman/werkzeugkasten/tool" 9 | "github.com/allaman/werkzeugkasten/tui/item" 10 | "github.com/allaman/werkzeugkasten/tui/keys" 11 | "github.com/allaman/werkzeugkasten/tui/styles" 12 | "github.com/charmbracelet/lipgloss" 13 | 14 | "github.com/charmbracelet/bubbles/help" 15 | "github.com/charmbracelet/bubbles/key" 16 | "github.com/charmbracelet/bubbles/list" 17 | "github.com/charmbracelet/bubbles/viewport" 18 | tea "github.com/charmbracelet/bubbletea" 19 | ) 20 | 21 | type MainModel struct { 22 | CurrentView string 23 | ToolsListView list.Model 24 | SelectedTool item.Tool 25 | DetailView Output 26 | ReleasesListView list.Model 27 | ProcessingModel Output 28 | ToolData tool.ToolData 29 | config cli.CliConfig 30 | version string 31 | } 32 | 33 | type Output struct { 34 | ItemName string 35 | ItemTag string 36 | ViewPort viewport.Model 37 | Content string 38 | Help help.Model 39 | } 40 | 41 | func InitialModel(toolData tool.ToolData, cfg cli.CliConfig) *MainModel { 42 | items := make([]list.Item, 0, len(toolData.Tools)) 43 | sortedTools := tool.SortTools(toolData) 44 | 45 | for _, tool := range sortedTools { 46 | identifier := toolData.Tools[tool].Identifier 47 | description := toolData.Tools[tool].Description 48 | items = append(items, item.NewItem(tool, identifier, description)) 49 | } 50 | 51 | l := list.New(items, list.NewDefaultDelegate(), 0, 0) 52 | l.Title = "Available Tools" 53 | l.AdditionalShortHelpKeys = func() []key.Binding { 54 | return keys.ToolsKeys.ShortHelp() 55 | } 56 | l.AdditionalFullHelpKeys = func() []key.Binding { 57 | return keys.ToolsKeys.FullHelp() 58 | } 59 | 60 | view := viewport.New(80, 20) 61 | 62 | return &MainModel{ 63 | config: cfg, 64 | CurrentView: "tools", 65 | ToolsListView: l, 66 | ToolData: toolData, 67 | DetailView: Output{ViewPort: view, Help: help.New()}, 68 | ReleasesListView: list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0), 69 | ProcessingModel: Output{ViewPort: view, Help: help.New()}, 70 | version: cli.Version, 71 | } 72 | } 73 | 74 | func (m MainModel) headerView() string { 75 | var title, line string 76 | if m.CurrentView == "detail" { 77 | title = styles.TitleStyle.Render("README of", m.DetailView.ItemName) 78 | line = strings.Repeat("─", max(0, m.DetailView.ViewPort.Width-lipgloss.Width(title))) 79 | } 80 | if m.CurrentView == "processing" { 81 | title = styles.TitleStyle.Render("Installing", m.ProcessingModel.ItemName) 82 | } 83 | return lipgloss.JoinHorizontal(lipgloss.Center, title, line) 84 | } 85 | 86 | func (m MainModel) footerView() string { 87 | var info, line string 88 | if m.CurrentView == "detail" { 89 | info = styles.InfoStyle.Render(fmt.Sprintf("%3.f%%", m.DetailView.ViewPort.ScrollPercent()*100)) 90 | line = strings.Repeat("─", max(0, m.DetailView.ViewPort.Width-lipgloss.Width(info))) 91 | } 92 | return lipgloss.JoinHorizontal(lipgloss.Center, line, info) 93 | } 94 | 95 | func (m *MainModel) showVersion() string { 96 | return cli.Version 97 | } 98 | 99 | func (m MainModel) Init() tea.Cmd { 100 | return tea.SetWindowTitle("Werkzeugkasten") 101 | } 102 | -------------------------------------------------------------------------------- /tool/tools.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | "os/exec" 9 | "runtime" 10 | "slices" 11 | "strings" 12 | "text/tabwriter" 13 | 14 | "github.com/goccy/go-yaml" 15 | ) 16 | 17 | //go:embed tools.yaml 18 | var toolsYAML []byte 19 | 20 | type ToolData struct { 21 | Tools map[string]Tool `yaml:"tools"` 22 | } 23 | 24 | type Tool struct { 25 | Identifier string `yaml:"identifier"` 26 | Tag string `yaml:"tag"` 27 | Categories []string `yaml:"categories"` 28 | Description string `yaml:"description"` 29 | AssetFilters []string `yaml:"asset_filters"` 30 | File string `yaml:"file"` 31 | } 32 | 33 | func CreateToolData() (ToolData, error) { 34 | var tools ToolData 35 | err := yaml.Unmarshal(toolsYAML, &tools) 36 | if err != nil { 37 | return ToolData{}, err 38 | } 39 | 40 | // Overwrite tags based with ENV variables 41 | // WK_TOOL_NAME_TAG, e.g. WK_KUSTOMIZE_TAG=v5.3.0 42 | for _, e := range os.Environ() { 43 | pair := strings.SplitN(e, "=", 2) 44 | key := pair[0] 45 | value := pair[1] 46 | 47 | if strings.HasPrefix(key, "WK_") { 48 | trimmedKey := strings.TrimPrefix(key, "WK_") 49 | splittedKey := strings.Split(trimmedKey, "_") 50 | if len(splittedKey) != 2 { 51 | slog.Warn("ignoring environment variable", "var", key) 52 | continue 53 | } 54 | tool := strings.ToLower(splittedKey[0]) 55 | field := strings.ToLower(splittedKey[1]) 56 | if field != "tag" { 57 | slog.Warn("ignoring malformed environment variable", "var", key) 58 | continue 59 | } 60 | if t, ok := tools.Tools[tool]; ok { 61 | slog.Debug("overwriting tag", "tool", tool, "tag", value) 62 | // tools.Tools[tool].Tag = value not working because 63 | // when modifying the fields of the struct obtained from the map, you are modifying a copy of the struct! 64 | t.Tag = value 65 | tools.Tools[tool] = t 66 | } 67 | } 68 | } 69 | return tools, nil 70 | } 71 | 72 | func execEget(workingDir string, tool Tool) ([]byte, error) { 73 | tag := tool.Tag 74 | name := tool.Identifier 75 | cmd := exec.Command("./eget", "-q", name, "--to", workingDir) 76 | if tag != "" { 77 | cmd = exec.Command("./eget", "-q", "-t", tag, name, "--to", workingDir) 78 | } 79 | if len(tool.AssetFilters) > 0 { 80 | for _, af := range tool.AssetFilters { 81 | cmd.Args = append(cmd.Args, fmt.Sprintf("--asset=%s", af)) 82 | } 83 | } 84 | if tool.File != "" { 85 | cmd.Args = append(cmd.Args, fmt.Sprintf("--file=\"%s\"", tool.File)) 86 | } 87 | cmd.Dir = workingDir 88 | slog.Debug("executing command", "cmd", cmd, "wd", cmd.Dir, "env", cmd.Env, "args", cmd.Args) 89 | out, err := cmd.CombinedOutput() 90 | return out, err 91 | } 92 | 93 | func DownloadToolWithEget(workingdir string, tool Tool) error { 94 | tool.Identifier = strings.Replace(tool.Identifier, "ARCH", runtime.GOARCH, 1) 95 | tool.Identifier = strings.Replace(tool.Identifier, "OSNAME", runtime.GOOS, 1) 96 | tag := "latest" 97 | if tool.Tag != "" { 98 | tag = tool.Tag 99 | } 100 | slog.Debug("downloading tool", "tool", tool.Identifier, "tag", tag) 101 | out, err := execEget(workingdir, tool) 102 | if err != nil { 103 | slog.Debug("could not download tool", "tool", tool.Identifier, "error", err, "out", string(out)) 104 | return err 105 | } 106 | return nil 107 | } 108 | 109 | func SortTools(tools ToolData) []string { 110 | sortedTools := make([]string, 0, len(tools.Tools)) 111 | for k := range tools.Tools { 112 | sortedTools = append(sortedTools, k) 113 | } 114 | slices.Sort(sortedTools) 115 | return sortedTools 116 | } 117 | 118 | func GetCategories(tools ToolData) map[string]int { 119 | categories := make(map[string]int, 0) 120 | for _, t := range tools.Tools { 121 | for _, c := range t.Categories { 122 | categories[c] = categories[c] + 1 123 | } 124 | } 125 | return categories 126 | } 127 | 128 | func PrintCategories(categories map[string]int) { 129 | sortedCategories := make([]string, 0, len(categories)) 130 | for k := range categories { 131 | sortedCategories = append(sortedCategories, k) 132 | } 133 | slices.Sort(sortedCategories) 134 | w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) 135 | fmt.Fprintln(w, "Name\tCount") 136 | for _, c := range sortedCategories { 137 | fmt.Fprintf(w, "%s\t%d\n", c, categories[c]) 138 | } 139 | w.Flush() 140 | } 141 | 142 | func GetToolsByCategory(category string, tools ToolData) ToolData { 143 | var toolsFound ToolData 144 | toolsFound.Tools = make(map[string]Tool, 0) 145 | lowerCategory := strings.ToLower(category) 146 | for k, t := range tools.Tools { 147 | lowerCategories := make([]string, len(t.Categories)) 148 | for i, v := range t.Categories { 149 | lowerCategories[i] = strings.ToLower(v) 150 | } 151 | if slices.Contains(lowerCategories, lowerCategory) { 152 | toolsFound.Tools[k] = t 153 | } 154 | } 155 | return toolsFound 156 | } 157 | 158 | func PrintTools(tools ToolData) { 159 | w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) 160 | fmt.Fprintln(w, "Key\tURL\tDescription") 161 | sortedTools := SortTools(tools) 162 | for _, tool := range sortedTools { 163 | identifier := tools.Tools[tool].Identifier 164 | url := fmt.Sprintf("https://github.com/%s", identifier) 165 | // handle packages that are not installed from GitHub 166 | if strings.HasPrefix(identifier, "https") { 167 | url = tools.Tools[tool].Identifier 168 | } 169 | fmt.Fprintf(w, "%s\t%s\t%s\n", tool, url, tools.Tools[tool].Description) 170 | } 171 | w.Flush() 172 | } 173 | -------------------------------------------------------------------------------- /tool/eget.go: -------------------------------------------------------------------------------- 1 | package tool 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "net/http" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | "runtime" 15 | "strings" 16 | ) 17 | 18 | type egetConfig struct { 19 | arch string 20 | os string 21 | url string 22 | version string 23 | } 24 | 25 | func newDefaultEgetConfig() egetConfig { 26 | url := "https://github.com/zyedidia/eget/releases/download/v%s/eget-%s-%s_%s.tar.gz" 27 | return egetConfig{arch: runtime.GOARCH, os: runtime.GOOS, url: url, version: "1.3.4"} 28 | } 29 | 30 | func createDir(path string) error { 31 | if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { 32 | slog.Debug(fmt.Sprintf("creating directory in %s", path)) 33 | if err := os.MkdirAll(path, 0777); err != nil { 34 | return fmt.Errorf("could not create directory %s", path) 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func downloadEgetBinary(dir string, c egetConfig) error { 41 | arch := c.arch 42 | operatingSystem := c.os 43 | version := c.version 44 | url := fmt.Sprintf(c.url, version, version, operatingSystem, arch) 45 | slog.Debug("downloading eget binary", "arch", arch, "os", operatingSystem, "version", version, "url", url) 46 | tmpDir, err := os.MkdirTemp(dir, "werkzeugkasten-") 47 | if err != nil { 48 | return err 49 | } 50 | slog.Debug(fmt.Sprintf("using temporary directory at %s", tmpDir)) 51 | if err := createDir(tmpDir); err != nil { 52 | return err 53 | } 54 | egetTar := path.Join(tmpDir, "eget.tar.gz") 55 | err = downloadFile(url, egetTar) 56 | if err != nil { 57 | return err 58 | } 59 | err = extractTarGz(egetTar, tmpDir, 1) 60 | if err != nil { 61 | return err 62 | } 63 | err = makeExecutable(path.Join(tmpDir, "eget")) 64 | if err != nil { 65 | return err 66 | } 67 | err = rename(path.Join(tmpDir, "eget"), path.Join(dir, "eget")) 68 | if err != nil { 69 | return err 70 | } 71 | err = os.RemoveAll(tmpDir) 72 | if err != nil { 73 | return err 74 | } 75 | return nil 76 | } 77 | 78 | func downloadFile(url, filepath string) error { 79 | // Create the file 80 | out, err := os.Create(filepath) 81 | if err != nil { 82 | return err 83 | } 84 | defer out.Close() 85 | 86 | // Get the data 87 | resp, err := http.Get(url) 88 | if err != nil { 89 | return err 90 | } 91 | defer resp.Body.Close() 92 | 93 | if resp.StatusCode != http.StatusOK { 94 | return fmt.Errorf("download of %s failed with status %d and error %s", url, resp.StatusCode, err) 95 | } 96 | // Write the body to file 97 | _, err = io.Copy(out, resp.Body) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func stripLeadingComponents(path string, components int) string { 106 | parts := strings.Split(path, "/") 107 | if components >= len(parts) { 108 | return "" 109 | } 110 | return filepath.Join(parts[components:]...) 111 | } 112 | 113 | // extractTarGz extracts a tar.gz archive to a destination directory with stripping leading path components. 114 | func extractTarGz(filePath, destPath string, stripComponents int) error { 115 | // Open the tar.gz file 116 | file, err := os.Open(filePath) 117 | if err != nil { 118 | return err 119 | } 120 | defer file.Close() 121 | 122 | // Create a gzip reader 123 | gzipReader, err := gzip.NewReader(file) 124 | if err != nil { 125 | return err 126 | } 127 | defer gzipReader.Close() 128 | 129 | // Create a tar reader 130 | tarReader := tar.NewReader(gzipReader) 131 | 132 | // Iterate through the files in the tar archive 133 | for { 134 | header, err := tarReader.Next() 135 | 136 | // If no more files are found, break out of the loop 137 | if err == io.EOF { 138 | break 139 | } 140 | if err != nil { 141 | return err 142 | } 143 | 144 | // Strip leading components from the file path 145 | strippedPath := stripLeadingComponents(header.Name, stripComponents) 146 | if strippedPath == "" { 147 | continue 148 | } 149 | path := filepath.Join(destPath, strippedPath) 150 | 151 | // Check the type of the file 152 | switch header.Typeflag { 153 | case tar.TypeDir: // if it's a directory 154 | if err := os.MkdirAll(path, 0755); err != nil { 155 | return err 156 | } 157 | case tar.TypeReg: // if it's a file 158 | // Create all directories for the file 159 | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 160 | return err 161 | } 162 | // Create the file 163 | outFile, err := os.Create(path) 164 | if err != nil { 165 | return err 166 | } 167 | // Copy the file data from the tar archive 168 | if _, err := io.Copy(outFile, tarReader); err != nil { 169 | outFile.Close() 170 | return err 171 | } 172 | outFile.Close() 173 | } 174 | } 175 | return nil 176 | } 177 | 178 | func makeExecutable(filePath string) error { 179 | return os.Chmod(filePath, 0755) 180 | } 181 | 182 | func rename(source, dest string) error { 183 | return os.Rename(source, dest) 184 | } 185 | 186 | func InstallEget(installDir string) { 187 | if _, err := os.Stat(path.Join(installDir, "eget")); errors.Is(err, os.ErrNotExist) { 188 | egetConfig := newDefaultEgetConfig() 189 | if os.Getenv("WK_EGET_VERSION") != "" { 190 | version := os.Getenv("WK_EGET_VERSION") 191 | slog.Debug("setting eget version", "version", version) 192 | egetConfig.version = version 193 | } 194 | err := downloadEgetBinary(installDir, egetConfig) 195 | if err != nil { 196 | slog.Error("could not download eget binary", "error", err) 197 | os.Exit(1) 198 | } 199 | } else { 200 | slog.Debug("eget allready downloaded") 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Werkzeugkasten 🧰

2 | 3 |
4 |

5 | Release 6 | size 7 | issues 8 | last commit 9 | license 10 | last release 11 |

12 | Conveniently download your favorite binaries (currently 152 supported)! 13 |
14 | 15 | ![screenshot](https://s1.gifyu.com/images/SBpu4.png) 16 | 17 |
18 | Open a tool's README within werkzeugkasten 19 | 20 | ![readme.png](https://s11.gifyu.com/images/SBpu5.png) 21 | 22 |
23 | 24 |
25 | Install a specific version 26 | 27 | ![release.gif](https://s14.gifyu.com/images/bsA18.gif) 28 | 29 |
30 | 31 |
32 | List categories and tool count 33 | 34 | `werkzeugkasten -categories` 35 | 36 | ![categories.png](https://s1.gifyu.com/images/SBpuN.png) 37 | 38 |
39 | 40 |
41 | List tools in category "Text" 42 | 43 | `werkzeugkasten -category text` 44 | 45 | ![category.png](https://s1.gifyu.com/images/SBpu9.png) 46 | 47 |
48 | 49 |
50 | Install tools in non-interactive mode 51 | 52 | ```sh 53 | export WK_FLUX2_TAG=2.1.0 # optionally specify version 54 | werkzeugkasten -dir ~/.local/bin -debug -tool flux2 55 | ``` 56 | 57 | ![install.png](https://s1.gifyu.com/images/SBpuv.png) 58 | 59 |
60 | 61 | From time to time, I need to connect to containers and VMs to troubleshoot them. These systems typically only have the necessary tools for their specific purpose and nothing else. Additionally, there is no root account available, so installing tools through a package manager is not an option. Furthermore, some tools are either not available as a package or the packaged version is outdated. 62 | 63 | This is where Werkzeugkasten comes in. You simply need to download the werkzeugkasten binary onto your system, and from that point on, there are no additional requirements, particularly the need for root permissions. 64 | 65 | ## Get Werkzeugkasten 66 | 67 | Unfortunately, a tool to download the werkzeugkasten binary is required. It is possible to download files via bash and `/dev/tcp` **only**, but I couldn't figure out how to handle the redirect from Github when accessing a release URL. 68 | 69 | with curl 70 | 71 | ```sh 72 | VERSION=$(curl -s https://api.github.com/repos/allaman/werkzeugkasten/releases/latest | grep tag_name | cut -d '"' -f 4) 73 | curl -sLo werkzeugkasten https://github.com/Allaman/werkzeugkasten/releases/download/${VERSION}/werkzeugkasten_${VERSION}_$(uname -s)_$(uname -m) 74 | ``` 75 | 76 | with wget 77 | 78 | ```sh 79 | VERSION=$(wget -qO - https://api.github.com/repos/allaman/werkzeugkasten/releases/latest | grep tag_name | cut -d '"' -f 4) 80 | wget -qO werkzeugkasten https://github.com/Allaman/werkzeugkasten/releases/download/${VERSION}/werkzeugkasten_${VERSION}_$(uname -s)_$(uname -m) 81 | ``` 82 | 83 | ```sh 84 | chmod +x werkzeugkasten 85 | ./werkzeugkasten 86 | ``` 87 | 88 | You could also integrate werkzeugkasten in your golden (Docker) image. ⚠️ Keep possible security implications in mind. 89 | 90 | ## How it works 91 | 92 | Werkzeugkasten is basically a wrapper around the excellent [eget](https://github.com/zyedidia/eget) that does the heavy lifting and is responsible for downloading the chosen tools. Eget itself is downloaded as binary via `net/http` call and decompression/extraction logic. 93 | 94 | The awesome [charmbracelet](https://github.com/charmbracelet) tools [bubbletea](https://github.com/charmbracelet/bubbletea), [glamour](https://github.com/charmbracelet/glamour), and [lipgloss](https://github.com/charmbracelet/lipgloss) are used for a modern look and feel. 95 | 96 | ## What Werkzeugkasten is not 97 | 98 | Werkzeugkasten is not intended to replace package managers (such as apt, brew, ...) or configuration management tools (such as Ansible, ...). 99 | 100 | ## Usage 101 | 102 | ```sh 103 | ❯ werkzeugkasten -help 104 | Usage: werkzeugkasten [flags] 105 | Flags: 106 | -categories 107 | Print all categories and tool count 108 | -category string 109 | List tools by category 110 | -debug 111 | Enable debug output 112 | -dir string 113 | Where to download the tools (default ".") 114 | -help 115 | Print help message 116 | -tool value 117 | Specify multiple tools to install programmatically (e.g., -tool kustomize -tool task) 118 | -tools 119 | Print all available tools 120 | -update 121 | Self-update 122 | -version 123 | Print version 124 | ``` 125 | 126 | Werkzeugkasten supports an **interactive** mode and a **non-interactive** mode. 127 | 128 | - `werkzeugkasten` will start in interactive mode where you select your tools (and version) you want to install from a searchable list. 129 | 130 | - `werkzeugkasten -tool age -tool kustomize` will download age and kustomize (latest release for both). 131 | 132 | - `werkzeugkasten -tools` will print all available tools. 133 | 134 | - `werkzeugkasten -categories` will print all available categories. 135 | 136 | - `werkzeugkasten -category network` will print all available tools in the "network" category. 137 | 138 | ## Configuration 139 | 140 | Besides CLI flags, further configuration is possible with environment variables. 141 | 142 | Set a tool's version/tag explicitly: 143 | 144 | ```sh 145 | export WK__=1.33.7 146 | export WK_KUSTOMIZE_TAG=v5.3.0` 147 | ``` 148 | 149 | Set a GitHub token to get more than the 60 API calls per hour limit: 150 | 151 | ```sh 152 | export EGET_GITHUB_TOKEN= 153 | ``` 154 | -------------------------------------------------------------------------------- /tui/model/update.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/allaman/werkzeugkasten/tui/item" 8 | "github.com/allaman/werkzeugkasten/tui/keys" 9 | 10 | "github.com/charmbracelet/bubbles/key" 11 | "github.com/charmbracelet/bubbles/list" 12 | tea "github.com/charmbracelet/bubbletea" 13 | ) 14 | 15 | func (m *MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 16 | switch msg := msg.(type) { 17 | case tea.WindowSizeMsg: 18 | m.ToolsListView.SetWidth(msg.Width) 19 | m.ToolsListView.SetHeight(msg.Height) 20 | m.DetailView.ViewPort.Width = msg.Width - 4 21 | m.DetailView.ViewPort.Height = msg.Height - 4 22 | m.ProcessingModel.ViewPort.Width = msg.Width - 4 23 | m.ProcessingModel.ViewPort.Height = msg.Height - 4 24 | 25 | case tea.KeyMsg: 26 | if m.ToolsListView.FilterState() == list.Filtering { 27 | break 28 | } 29 | 30 | switch m.CurrentView { 31 | 32 | case "tools": 33 | switch { 34 | 35 | case key.Matches(msg, keys.ToolsKeys.Install): 36 | selectedItem, ok := m.ToolsListView.SelectedItem().(item.Tool) 37 | if ok { 38 | m.CurrentView = "processing" 39 | m.ProcessingModel.ItemName = selectedItem.Title() 40 | return m, m.processSelectedItem() 41 | } 42 | 43 | case key.Matches(msg, keys.ToolsKeys.Describe): 44 | selectedItem, ok := m.ToolsListView.SelectedItem().(item.Tool) 45 | if ok { 46 | m.CurrentView = "detail" 47 | m.DetailView.ItemName = selectedItem.Title() 48 | return m, fetchReadmeCmd(fmt.Sprintf("https://raw.githubusercontent.com/%s/main/README.md", selectedItem.Identifier())) 49 | } 50 | 51 | case key.Matches(msg, keys.ToolsKeys.Releases): 52 | selectedItem, ok := m.ToolsListView.SelectedItem().(item.Tool) 53 | if ok { 54 | m.CurrentView = "releases" 55 | m.SelectedTool = selectedItem 56 | m.ReleasesListView.Title = fmt.Sprintf("Releases of %s (max. last 100)", selectedItem.Title()) 57 | m.ReleasesListView.AdditionalFullHelpKeys = func() []key.Binding { 58 | return []key.Binding{ 59 | keys.ReleasesKeys.Install, 60 | keys.ReleasesKeys.Esc, 61 | } 62 | } 63 | return m, fetchReleasesCmd(selectedItem.Identifier()) 64 | } 65 | 66 | case key.Matches(msg, keys.ToolsKeys.Browse): 67 | selectedItem, ok := m.ToolsListView.SelectedItem().(item.Tool) 68 | if ok { 69 | return m, openBrowserCmd(selectedItem.Identifier()) 70 | } 71 | 72 | case key.Matches(msg, keys.ToolsKeys.Version): 73 | m.CurrentView = "version" 74 | return m, nil 75 | } 76 | 77 | case "detail": 78 | switch { 79 | case key.Matches(msg, keys.DetailKeys.Down): 80 | m.DetailView.ViewPort.ScrollDown(1) 81 | case key.Matches(msg, keys.DetailKeys.Up): 82 | m.DetailView.ViewPort.ScrollUp(1) 83 | case key.Matches(msg, keys.DetailKeys.HalfPageDown): 84 | m.DetailView.ViewPort.HalfPageDown() 85 | case key.Matches(msg, keys.DetailKeys.HalfPageUp): 86 | m.DetailView.ViewPort.HalfPageUp() 87 | case key.Matches(msg, keys.DetailKeys.Help): 88 | m.DetailView.Help.ShowAll = !m.DetailView.Help.ShowAll 89 | case key.Matches(msg, keys.DetailKeys.Install): 90 | selectedItem, ok := m.ToolsListView.SelectedItem().(item.Tool) 91 | if ok { 92 | m.CurrentView = "processing" 93 | m.ProcessingModel.ItemName = selectedItem.Title() 94 | return m, m.processSelectedItem() 95 | } 96 | case key.Matches(msg, keys.DetailKeys.Esc): 97 | m.CurrentView = "tools" 98 | return m, nil 99 | } 100 | 101 | case "releases": 102 | switch { 103 | case key.Matches(msg, keys.ReleasesKeys.Install): 104 | selectedItem, ok := m.ReleasesListView.SelectedItem().(item.Release) 105 | if ok { 106 | m.CurrentView = "processing" 107 | m.ProcessingModel.ItemName = m.SelectedTool.Title() 108 | m.ProcessingModel.ItemTag = selectedItem.Tag 109 | return m, m.processSelectedItem() 110 | } 111 | case key.Matches(msg, keys.ReleasesKeys.Esc): 112 | m.CurrentView = "tools" 113 | return m, nil 114 | } 115 | 116 | case "processing": 117 | switch { 118 | case key.Matches(msg, keys.ProcessingKeys.Esc): 119 | m.CurrentView = "tools" 120 | return m, nil 121 | case key.Matches(msg, keys.ProcessingKeys.Quit): 122 | return m, tea.Quit 123 | } 124 | 125 | case "version": 126 | if msg.String() == "esc" { 127 | m.CurrentView = "tools" 128 | return m, nil 129 | } 130 | } 131 | 132 | case fetchReadmeSuccessMsg: 133 | m.DetailView.ViewPort.SetContent(string(msg)) 134 | m.DetailView.ViewPort.GotoTop() 135 | return m, nil 136 | 137 | case fetchReadmeErrMsg: 138 | m.DetailView.ViewPort.SetContent(msg.err.Error()) 139 | return m, nil 140 | 141 | case fetchReleasesSuccessMsg: 142 | releases := []item.FetchRelease(msg) 143 | items := make([]list.Item, 0, len(releases)) 144 | 145 | for _, release := range releases { 146 | items = append(items, item.Release{Tag: release.TagName, PublishedAt: release.PublishedAt}) 147 | } 148 | // Update the existing list with the new items 149 | cmd := m.ReleasesListView.SetItems(items) 150 | 151 | // Make sure dimensions are correct 152 | m.ReleasesListView.SetWidth(m.ToolsListView.Width()) 153 | m.ReleasesListView.SetHeight(m.ToolsListView.Height()) 154 | 155 | // Return the command from SetItems to ensure proper updates 156 | return m, cmd 157 | 158 | case fetchReleasesErrMsg: 159 | errorItem := item.NewRelease("Error: "+msg.err.Error(), time.Now()) 160 | m.ReleasesListView.SetItems([]list.Item{errorItem}) 161 | return m, nil 162 | 163 | case processSuccessMsg: 164 | m.ProcessingModel.ViewPort.SetContent(string(msg)) 165 | m.ProcessingModel.ViewPort.GotoTop() 166 | return m, nil 167 | 168 | case processErrMsg: 169 | m.ProcessingModel.ViewPort.SetContent(msg.err.Error()) 170 | return m, nil 171 | 172 | case browserSuccessMsg: 173 | return m, nil 174 | 175 | case browserErrMsg: 176 | // TODO: handle error 177 | return m, nil 178 | 179 | } 180 | 181 | var cmd tea.Cmd 182 | var cmds []tea.Cmd 183 | 184 | switch m.CurrentView { 185 | case "tools": 186 | newToolsListView, toolsCmd := m.ToolsListView.Update(msg) 187 | m.ToolsListView = newToolsListView 188 | if toolsCmd != nil { 189 | cmds = append(cmds, toolsCmd) 190 | } 191 | case "releases": 192 | newReleasesListView, releasesCmd := m.ReleasesListView.Update(msg) 193 | m.ReleasesListView = newReleasesListView 194 | if releasesCmd != nil { 195 | cmds = append(cmds, releasesCmd) 196 | } 197 | } 198 | 199 | if len(cmds) > 0 { 200 | cmd = tea.Batch(cmds...) 201 | } 202 | return m, cmd 203 | } 204 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## What's Changed in 4.4.1 2 | * feat(tools): Add more tools by @Allaman 3 | * docs: Update CHANGELOG by @Allaman 4 | 5 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.4.0...4.4.1 6 | 7 | ## What's Changed in 4.4.0 8 | * chore: Update go and package versions by @Allaman 9 | 10 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.3.2...4.4.0 11 | 12 | ## What's Changed in 4.3.2 13 | * ci: deprecated archives.format in goreleaser by @Allaman 14 | 15 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.3.1...4.3.2 16 | 17 | ## What's Changed in 4.3.1 18 | * feat(tools): Add more tools by @Allaman 19 | * docs: Update CHANGELOG by @Allaman 20 | 21 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.3.0...4.3.1 22 | 23 | ## What's Changed in 4.3.0 24 | * fix: Pass GitHub token by @Allaman 25 | * feat: Add 'browse' by @Allaman 26 | * fix: Shorthelp and FullHelp in list view by @Allaman 27 | * docs: Update README by @Allaman 28 | * docs: Update CHANGELOG by @Allaman 29 | 30 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.2.3...4.3.0 31 | 32 | ## What's Changed in 4.2.3 33 | * feat(tools): Maintain tool list by @Allaman 34 | * docs: Update README by @Allaman 35 | * docs: Update README by @Allaman 36 | * docs: Update README by @Allaman 37 | 38 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.2.2...4.2.3 39 | 40 | ## What's Changed in 4.2.2 41 | * feat(tools): Add gecho by @Allaman 42 | * docs: Update CHANGELOG by @Allaman 43 | 44 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.2.1...4.2.2 45 | 46 | ## What's Changed in 4.2.1 47 | * feat(tools): Add telepresence by @Allaman 48 | * docs: Update CHANGELOG by @Allaman 49 | 50 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.2.0...4.2.1 51 | 52 | ## What's Changed in 4.2.0 53 | * fix: Debug output by @Allaman 54 | * feat(yaml): Add more tools by @Allaman 55 | * feat: Add interactive release selection by @Allaman 56 | * refactor: File renaming by @Allaman 57 | * chore: Update dependencies by @Allaman 58 | * chore: Replace gopkg.in/yaml with goccy/go-yaml by @Allaman 59 | * feat: Add releases view by @Allaman 60 | * fix: processing keybindings by @Allaman 61 | * fix: Deprecated viewport commands by @Allaman 62 | * chore: Update depedencies by @Allaman 63 | * docs: Update CHANGELOG by @Allaman 64 | 65 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.1.1...4.2.0 66 | 67 | ## What's Changed in 4.1.1 68 | * fix: Use MkdirTemp instead of hard-coded "tmp" as temporary directory by @Allaman 69 | * feat(tools): Add more tools by @Allaman 70 | * chore: Update dependencies and go by @Allaman 71 | * docs: Update CHANGELOG by @Allaman 72 | * chore: Update clif.toml by @Allaman 73 | 74 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.1.0...4.1.1 75 | 76 | ## What's Changed in 4.1.0 77 | * feat(yaml): Add more tools by @Allaman 78 | * fix: Pass config to bubbletea model #3 by @Allaman 79 | * docs: Update Changelog by @Allaman 80 | 81 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.0.6...4.1.0 82 | 83 | ## What's Changed in 4.0.6 84 | * feat(yaml): Add more tools by @Allaman 85 | * docs: Update Changelog by @Allaman 86 | 87 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.0.5...4.0.6 88 | 89 | ## What's Changed in 4.0.5 90 | * feat(tools): Add more tools by @Allaman 91 | * docs: Update Changelog by @Allaman 92 | 93 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.0.4...4.0.5 94 | 95 | ## What's Changed in 4.0.4 96 | * feat(tools): Add more tools by @Allaman 97 | * docs: Update README by @Allaman 98 | * docs: Update README by @Allaman 99 | * docs: Update Changelog by @Allaman 100 | 101 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.0.3...4.0.4 102 | 103 | ## What's Changed in 4.0.3 104 | * feat(tools): Add more tools by @Allaman 105 | * docs: Update Changelog by @Allaman 106 | 107 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.0.2...4.0.3 108 | 109 | ## What's Changed in 4.0.2 110 | * fix(yaml): Add binsider asset_filters by @Allaman 111 | * docs: Update Changelog by @Allaman 112 | 113 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.0.1...4.0.2 114 | 115 | ## What's Changed in 4.0.1 116 | * chore: Fix goreleaser.yml by @Allaman 117 | * docs: Update README.md by @Allaman 118 | * docs: Update Changelog by @Allaman 119 | 120 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/4.0.0...4.0.1 121 | 122 | ## What's Changed in 4.0.0 123 | * docs: Update README.md by @Allaman 124 | * feat!: Use bubbletea instead of huh by @Allaman in [#2](https://github.com/allaman/werkzeugkasten/pull/2) 125 | * docs: More and better screenshots by @Allaman 126 | * docs: Update CHANGELOG by @Allaman 127 | 128 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/3.1.0...4.0.0 129 | 130 | ## What's Changed in 3.1.0 131 | * feat(tools): Add more tools by @Allaman 132 | * docs: Update README by @Allaman 133 | * docs: Update README by @Allaman 134 | * docs: Update CHANGELOG by @Allaman 135 | 136 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/3.0.0...3.1.0 137 | 138 | ## What's Changed in 3.0.0 139 | * feat(tools): Add category flag by @Allaman 140 | * fix(tools): Handle optional -t argument properly by @Allaman 141 | * refactor(tools): Remove normalizedPath function by @Allaman 142 | * feat!: Add category listing flag and rename list flag by @Allaman 143 | * fix(tools): Typo by @Allaman 144 | * docs: Update Screenshot by @Allaman 145 | * fix(eget): Check if eget is allready downloaded by @Allaman 146 | * docs: Update CHANGELOG by @Allaman 147 | 148 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/2.1.0...3.0.0 149 | 150 | ## What's Changed in 2.1.0 151 | * refactor(ui): More idiomatic code by @Allaman 152 | * feat(cli): Make theme configurable by @Allaman 153 | * style: Remove obsolete comment by @Allaman 154 | * feat(tools): Add birdayz/kaf by @Allaman 155 | * feat(tools): Add containerd/nerdctl by @Allaman 156 | * fix: Typo in flag description by @Allaman 157 | * docs: Update README by @Allaman 158 | * docs: Update CHANGELOG by @Allaman 159 | 160 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/2.0.0...2.1.0 161 | 162 | ## What's Changed in 2.0.0 163 | * feat!: Implement non-interactive mode by @Allaman 164 | * fix(tools): Formatting by @Allaman 165 | * refactor(tools): Better variable names by @Allaman 166 | * fix(tools): Add asset_filters for FiloSottile/age by @Allaman 167 | * feat(eget): Bump eget version to 1.3.4 by @Allaman 168 | * refactor(tools): Rename createDefaultTools to createToolData by @Allaman 169 | * fix: Remove obsolet struct by @Allaman 170 | * dosc: Update README by @Allaman 171 | * docs: Update CHANGELOG by @Allaman 172 | 173 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/1.2.0...2.0.0 174 | 175 | ## What's Changed in 1.2.0 176 | * feat(tools): add ayoisaiah/f2 by @Allaman 177 | * docs: Update CHANGELOG by @Allaman 178 | 179 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/1.1.0...1.2.0 180 | 181 | ## What's Changed in 1.1.0 182 | * feat(tools): Add more tools by @Allaman 183 | * docs: Update CHANGELOG by @Allaman 184 | 185 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/1.0.2...1.1.0 186 | 187 | ## What's Changed in 1.0.2 188 | * feat(tools): add mr-karan/doggo by @Allaman 189 | * docs: Update CHANGELOG by @Allaman 190 | 191 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/1.0.1...1.0.2 192 | 193 | ## What's Changed in 1.0.1 194 | * refactor: UI code by @Allaman 195 | * fix: Idiomatic info message style by @Allaman 196 | * docs: Add CHANGELOG by @Allaman 197 | * chore(deps): bump goreleaser/goreleaser-action from 5 to 6 by @Allaman in [#1](https://github.com/allaman/werkzeugkasten/pull/1) 198 | * fix: Idiomatic error message style by @Allaman 199 | 200 | ## New Contributors 201 | * @dependabot[bot] made their first contribution 202 | 203 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/1.0.0...1.0.1 204 | 205 | ## What's Changed in 1.0.0 206 | * feat!: Set default tool versions to "latest" by @Allaman 207 | 208 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/0.14.0...1.0.0 209 | 210 | ## What's Changed in 0.14.0 211 | * feat(tools): Add more tools by @Allaman 212 | 213 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/0.13.0...0.14.0 214 | 215 | ## What's Changed in 0.13.0 216 | * feat(tools): Add altsem/gitu by @Allaman 217 | * feat(tools): Add GitHub CLI by @Allaman 218 | * feat(tools): Add Macchina-CLI/macchina by @Allaman 219 | 220 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/0.12.1...0.13.0 221 | 222 | ## What's Changed in 0.12.1 223 | * fix: jdx/mise asset_filter by @Allaman 224 | 225 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/0.12.0...0.12.1 226 | 227 | ## What's Changed in 0.12.0 228 | * docs: Update README by @Allaman 229 | * ci!: Use x86_64 instead of amd64 as release name by @Allaman 230 | * feat(tools): Add jdx/mise by @Allaman 231 | 232 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/0.11.0...0.12.0 233 | 234 | ## What's Changed in 0.11.0 235 | * feat(tools): Add alajmo/mani by @Allaman 236 | * fix(tools): Bump chkRedis tag by @Allaman 237 | * fix(tools): Add asset_filters for micro by @Allaman 238 | 239 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/0.10.3...0.11.0 240 | 241 | ## What's Changed in 0.10.3 242 | * ci: Fix goreleaser failing to write changelog by @Allaman 243 | 244 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/0.10.2...0.10.3 245 | 246 | ## What's Changed in 0.10.2 247 | * ci: Fix and improve goreleaser configuration by @Allaman 248 | 249 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/0.10.1...0.10.2 250 | 251 | ## What's Changed in 0.10.1 252 | * ci: fix goreleaser ldflags by @Allaman 253 | 254 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/0.10.0...0.10.1 255 | 256 | ## What's Changed in 0.10.0 257 | * feat: Add tools tools tools by @Allaman 258 | * docs: Update README by @Allaman 259 | * fix: Warn when finding malformed ENV var by @Allaman 260 | * docs: Update README by @Allaman 261 | * feat: Add cli flags by @Allaman 262 | * docs: Update screenshot by @Allaman 263 | * fix: Concise styling of tool selection list by @Allaman 264 | * fix: Better descriptions by @Allaman 265 | * docs: Update screenshot by @Allaman 266 | * refactor(eget): egetConfig -> config by @Allaman 267 | * feat: Sort tools alphabetically by @Allaman 268 | * chore: Update gitignore by @Allaman 269 | * docs: Update README by @Allaman 270 | * Merge branch 'main' of github.com:Allaman/werkzeugkasten by @Allaman 271 | * docs: Update README.md by @Allaman 272 | * docs: Create LICENSE by @Allaman 273 | * docs: Update README.md by @Allaman 274 | * docs: Add screenshot by @Allaman 275 | * ci: Update Taskfile by @Allaman 276 | * docs: Update README by @Allaman 277 | * chore: Update packages by @Allaman 278 | * fix: convert []byte to string for debug log by @Allaman 279 | * fix: Handle identifiers with OS/ARCH by @Allaman 280 | * docs: Add README by @Allaman 281 | 282 | **Full Changelog**: https://github.com/allaman/werkzeugkasten/compare/0.9.0...0.10.0 283 | 284 | ## What's Changed in 0.9.0 285 | * ci: Add workflow by @Allaman 286 | * initial commit by @Allaman 287 | 288 | ## New Contributors 289 | * @Allaman made their first contribution 290 | 291 | 292 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= 2 | code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= 3 | code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0= 4 | code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= 5 | github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= 6 | github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= 7 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= 8 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 9 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 10 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 11 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 12 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 13 | github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI= 14 | github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 15 | github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= 16 | github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= 17 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 18 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 19 | github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= 20 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 21 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 22 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 23 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 24 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 25 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 26 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 27 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 28 | github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= 29 | github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= 30 | github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= 31 | github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= 32 | github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= 33 | github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= 34 | github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= 35 | github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= 36 | github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= 37 | github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= 38 | github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= 39 | github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= 40 | github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= 41 | github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= 42 | github.com/charmbracelet/x/ansi v0.9.2 h1:92AGsQmNTRMzuzHEYfCdjQeUzTrgE1vfO5/7fEVoXdY= 43 | github.com/charmbracelet/x/ansi v0.9.2/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 44 | github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= 45 | github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 46 | github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= 47 | github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 48 | github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= 49 | github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 50 | github.com/charmbracelet/x/exp/slice v0.0.0-20250514204301-7f4ee4d0d5fe h1:qPQM6zDAPkfY0yt4T9wjxbUFXHrJ/T4s7Cnr5dhx0kQ= 51 | github.com/charmbracelet/x/exp/slice v0.0.0-20250514204301-7f4ee4d0d5fe/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms= 52 | github.com/charmbracelet/x/exp/slice v0.0.0-20250919153222-1038f7e6fef4 h1:zXwT6La0lwA7i4LESGS137z0XRGIbIrX3TAEkRwvpCw= 53 | github.com/charmbracelet/x/exp/slice v0.0.0-20250919153222-1038f7e6fef4/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= 54 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 55 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 56 | github.com/creativeprojects/go-selfupdate v1.5.0 h1:4zuFafc/qGpymx7umexxth2y2lJXoBR49c3uI0Hr+zU= 57 | github.com/creativeprojects/go-selfupdate v1.5.0/go.mod h1:Pewm8hY7Xe1ne7P8irVBAFnXjTkRuxbbkMlBeTdumNQ= 58 | github.com/creativeprojects/go-selfupdate v1.5.1 h1:fuyEGFFfqcC8SxDGolcEPYPLXGQ9Mcrc5uRyRG2Mqnk= 59 | github.com/creativeprojects/go-selfupdate v1.5.1/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM= 60 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 61 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 62 | github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= 63 | github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= 64 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 65 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 66 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 67 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 68 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 69 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 70 | github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= 71 | github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= 72 | github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= 73 | github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 74 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 75 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 76 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 77 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 78 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 79 | github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo= 80 | github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8= 81 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 82 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 83 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 84 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 85 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 86 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 87 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 88 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 89 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 90 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 91 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 92 | github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= 93 | github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= 94 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 95 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 96 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 97 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 98 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 99 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 100 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 101 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 102 | github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= 103 | github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 104 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 105 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 106 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 107 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 108 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 109 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 110 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 111 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 112 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 113 | github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 114 | github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 115 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 116 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 117 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 118 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 119 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 120 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 121 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 122 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 123 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 124 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 125 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 126 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 127 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 128 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 129 | github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= 130 | github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 131 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 132 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 133 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 134 | github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= 135 | github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 136 | github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= 137 | github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 138 | github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8= 139 | github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M= 140 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 141 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 142 | github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo= 143 | github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 144 | github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= 145 | github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 146 | github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= 147 | github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= 148 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 149 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 150 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 151 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 152 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 153 | golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= 154 | golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= 155 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 156 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 157 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 158 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 159 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 160 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 161 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 162 | golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= 163 | golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 164 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 165 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 166 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 167 | golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= 168 | golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 169 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 170 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 171 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 172 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 173 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 177 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 178 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 179 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 180 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 181 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 182 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 183 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 184 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 185 | golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= 186 | golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 187 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 188 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 189 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 190 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 191 | golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= 192 | golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 193 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 194 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 195 | golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= 196 | golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 197 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 198 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 199 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 200 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 201 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 202 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 203 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 204 | -------------------------------------------------------------------------------- /tool/tools.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | tools: 3 | kustomize: 4 | identifier: "kubernetes-sigs/kustomize" 5 | description: "Customization of kubernetes YAML configurations." 6 | categories: ["Kubernetes", "GitOps"] 7 | flux2: 8 | identifier: "fluxcd/flux2" 9 | description: "Open and extensible continuous delivery solution for Kubernetes. Powered by GitOps Toolkit." 10 | categories: ["Kubernetes", "GitOps"] 11 | k6: 12 | identifier: "grafana/k6" 13 | description: "A modern load testing tool, using Go and JavaScript." 14 | categories: ["Development", "Testing"] 15 | kubeconform: 16 | identifier: "yannh/kubeconform" 17 | description: "A FAST Kubernetes manifests validator, with support for Custom Resources!" 18 | categories: ["Kubernetes", "Validation"] 19 | kubent: 20 | identifier: "doitintl/kube-no-trouble" 21 | description: "Easily check your clusters for use of deprecated APIs." 22 | categories: ["Kubernetes", "Validation"] 23 | kyverno: 24 | identifier: "kyverno/kyverno" 25 | description: "Kubernetes Native Policy Management." 26 | asset_filters: ["^sig", "^pem", ".tar.gz"] 27 | categories: ["Kubernetes", "Validation"] 28 | fd: 29 | identifier: "sharkdp/fd" 30 | description: "A simple, fast and user-friendly alternative to 'find'." 31 | categories: ["Files"] 32 | ripgrep: 33 | identifier: "BurntSushi/ripgrep" 34 | description: "ripgrep recursively searches directories for a regex pattern while respecting your gitignore." 35 | categories: ["Text"] 36 | bat: 37 | identifier: "sharkdp/bat" 38 | description: "A cat(1) clone with wings." 39 | categories: ["Text"] 40 | curlie: 41 | identifier: "rs/curlie" 42 | description: "The power of curl, the ease of use of httpie." 43 | categories: ["Network"] 44 | dua: 45 | identifier: "Byron/dua-cli" 46 | description: "View disk space usage and delete unwanted data, fast." 47 | categories: ["System"] 48 | gojq: 49 | identifier: "itchyny/gojq" 50 | description: "Pure Go implementation of jq." 51 | categories: ["Json"] 52 | jaq: 53 | identifier: "01mf02/jaq" 54 | description: "A jq clone focussed on correctness, speed, and simplicity." 55 | categories: ["Json"] 56 | bottom: 57 | identifier: "ClementTsang/bottom" 58 | description: "Yet another cross-platform graphical process/system monitor." 59 | file: "btm" 60 | categories: ["System"] 61 | dnslookup: 62 | identifier: "ameshkov/dnslookup" 63 | description: "Simple command line utility to make DNS lookups to the specified server." 64 | categories: ["Network"] 65 | procs: 66 | identifier: "dalance/procs" 67 | description: "A modern replacement for ps written in Rust." 68 | categories: ["System"] 69 | sd: 70 | identifier: "chmln/sd" 71 | description: "Intuitive find & replace CLI (sed alternative)." 72 | categories: ["Text"] 73 | zellij: 74 | identifier: "zellij-org/zellij" 75 | description: "A terminal workspace with batteries included (screen/tmux alternative)." 76 | categories: ["Shell"] 77 | micro: 78 | identifier: "zyedidia/micro" 79 | description: "A modern and intuitive terminal-based text editor." 80 | asset_filters: ["static"] 81 | categories: ["Text"] 82 | helix: 83 | identifier: "helix-editor/helix" 84 | description: "A post-modern modal text editor." 85 | categories: ["Text"] 86 | xplr: 87 | identifier: "sayanarijit/xplr" 88 | description: "A hackable, minimal, fast TUI file explorer." 89 | asset_filters: ["^asc"] 90 | categories: ["Files"] 91 | fzf: 92 | identifier: "junegunn/fzf" 93 | description: "A command-line fuzzy finder." 94 | categories: ["Text"] 95 | s5cmd: 96 | identifier: "peak/s5cmd" 97 | description: "Parallel S3 and local filesystem execution tool." 98 | categories: ["Cloud"] 99 | task: 100 | identifier: "go-task/task" 101 | description: "A task runner / simpler Make alternative written in Go." 102 | asset_filters: [".tar.gz"] 103 | categories: ["Shell"] 104 | polaris: 105 | identifier: "FairwindsOps/polaris" 106 | description: "Validation of best practices in your Kubernetes clusters." 107 | categories: ["Kubernetes", "Validation"] 108 | yamlfmt: 109 | identifier: "google/yamlfmt" 110 | description: "An extensible command line tool or library to format yaml files." 111 | categories: ["Yaml"] 112 | kube-linter: 113 | identifier: "stackrox/kube-linter" 114 | description: "KubeLinter is a static analysis tool that checks Kubernetes YAML files and Helm charts to ensure the applications represented in them adhere to best practices." 115 | asset_filters: [".tar.gz"] 116 | categories: ["Kubernetes", "Validation"] 117 | lazygit: 118 | identifier: "jesseduffield/lazygit" 119 | description: "simple terminal UI for git commands." 120 | categories: ["Git"] 121 | k9s: 122 | identifier: "derailed/k9s" 123 | description: "Kubernetes CLI To Manage Your Clusters In Style!" 124 | asset_filters: ["^sbom"] 125 | categories: ["Kubernetes"] 126 | lazydocker: 127 | identifier: "jesseduffield/lazydocker" 128 | description: "The lazier way to manage everything docker." 129 | categories: ["Docker"] 130 | gping: 131 | identifier: "orf/gping" 132 | description: "Ping, but with a graph." 133 | categories: ["Network"] 134 | stern: 135 | identifier: "stern/stern" 136 | description: "Multi pod and container log tailing for Kubernetes." 137 | categories: ["Kubernetes", "Development"] 138 | lazysql: 139 | identifier: "jorgerojas26/lazysql" 140 | description: "A cross-platform TUI database management tool written in Go." 141 | categories: ["Development", "Database"] 142 | dive: 143 | identifier: "wagoodman/dive" 144 | description: "A tool for exploring each layer in a docker image." 145 | categories: ["Docker"] 146 | yazi: 147 | identifier: "sxyazi/yazi" 148 | description: "Blazing fast terminal file manager written in Rust, based on async I/O." 149 | categories: ["Files"] 150 | glow: 151 | identifier: "charmbracelet/glow" 152 | description: "Render markdown on the CLI, with pizzazz!" 153 | asset_filters: ["^sbom"] 154 | categories: ["Markdown"] 155 | lf: 156 | identifier: "gokcehan/lf" 157 | description: "Terminal file manager." 158 | categories: ["Files"] 159 | chkRedis: 160 | identifier: "Allaman/chkRedis" 161 | description: "A minimal helper tool written in Go to verify the connection to a Redis data store." 162 | categories: ["Development"] 163 | tfswitch: 164 | identifier: "warrensbox/terraform-switcher" 165 | description: "A command line tool to switch (and install) between different versions of terraform." 166 | categories: ["Cloud"] 167 | file: "tfswitch" 168 | tenv: 169 | identifier: "tofuutils/tenv" 170 | description: "OpenTofu / Terraform / Terragrunt version manager." 171 | categories: ["Cloud"] 172 | golang: 173 | # TODO: version handling 174 | identifier: "https://go.dev/dl/go1.17.5.OSNAME-ARCH.tar.gz" 175 | description: "The Golang programming language." 176 | file: "go/bin/go" 177 | categories: ["Development"] 178 | hexyl: 179 | identifier: "sharkdp/hexyl" 180 | description: "A command-line hex viewer." 181 | categories: ["Development", "Files"] 182 | ouch: 183 | identifier: "ouch-org/ouch" 184 | description: "Painless compression and decompression in the terminal." 185 | categories: ["Files"] 186 | miller: 187 | identifier: "johnkerl/miller" 188 | description: "Miller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON." 189 | categories: ["CSV"] 190 | xsv: 191 | identifier: "BurntSushi/xsv" 192 | description: "A fast CSV command line toolkit written in Rust. (archived)" 193 | categories: ["CSV"] 194 | terrascan: 195 | identifier: "tenable/terrascan" 196 | description: "Detect compliance and security violations across Infrastructure as Code to mitigate risk before provisioning cloud native infrastructure." 197 | categories: ["Cloud"] 198 | tfsec: 199 | identifier: "aquasecurity/tfsec" 200 | description: "Security scanner for your Terraform code." 201 | asset_filters: ["^checkgen", "^sig", "^tar.gz"] 202 | categories: ["Cloud"] 203 | argocd: 204 | identifier: "argoproj/argo-cd" 205 | description: "Declarative Continuous Deployment for Kubernetes." 206 | categories: ["Kubernetes", "GitOps"] 207 | dog: 208 | identifier: "ogham/dog" 209 | description: "A command-line DNS client." 210 | asset_filters: ["^minisig"] 211 | categories: ["Network"] 212 | hwatch: 213 | identifier: "blacknon/hwatch" 214 | description: "A modern alternative to the watch command, records the differences in execution results and can check this differences at after." 215 | categories: ["System"] 216 | viddy: 217 | identifier: "sachaos/viddy" 218 | description: "A modern watch command. Time machine and pager etc." 219 | categories: ["System"] 220 | gron: 221 | identifier: "tomnomnom/gron" 222 | description: "Make JSON greppable!" 223 | categories: ["Json"] 224 | helm: 225 | identifier: "https://get.helm.sh/helm-v3.12.3-OSNAME-ARCH.tar.gz" 226 | description: "The Kubernetes Package Manager." 227 | file: "helm" 228 | categories: ["Kubernetes"] 229 | hurl: 230 | identifier: "Orange-OpenSource/hurl" 231 | description: "Hurl, run and test HTTP requests with plain text." 232 | categories: ["Development", "Testing"] 233 | kubeseal: 234 | identifier: "bitnami-labs/sealed-secrets" 235 | description: "A Kubernetes controller and tool for one-way encrypted Secrets." 236 | asset_filters: ["^sig"] 237 | file: "kubeseal" 238 | categories: ["Kubernetes"] 239 | enc: 240 | identifier: "life4/enc" 241 | description: "A modern and friendly CLI alternative to GnuPG: generate and download keys, encrypt, decrypt, and sign text and files, and more." 242 | categories: ["System"] 243 | age: 244 | identifier: "FiloSottile/age" 245 | description: "A simple, modern and secure encryption tool (and Go library) with small explicit keys, no config options, and UNIX-style composability." 246 | asset_filters: ["^proof"] 247 | categories: ["System"] 248 | podman: 249 | identifier: "containers/podman" 250 | description: "Podman: A tool for managing OCI containers and pods." 251 | asset_filters: ["^pkg"] 252 | categories: ["Docker"] 253 | bandwhich: 254 | identifier: "imsnif/bandwhich" 255 | description: "Terminal bandwidth utilization tool." 256 | categories: ["Network"] 257 | trippy: 258 | identifier: "fujiapple852/trippy" 259 | description: "A network diagnostic tool." 260 | categories: ["Network"] 261 | miniserve: 262 | identifier: "svenstaro/miniserve" 263 | description: "For when you really just want to serve some files over HTTP right now!" 264 | categories: ["Network"] 265 | trivy: 266 | identifier: "aquasecurity/trivy" 267 | description: "Find vulnerabilities, misconfigurations, secrets, SBOM in containers, Kubernetes, code repositories, clouds and more." 268 | asset_filters: ["^pem", "^sig"] 269 | categories: ["Cloud", "Validation"] 270 | process_compose: 271 | identifier: "F1bonacc1/process-compose" 272 | description: "Process Compose is a simple and flexible scheduler and orchestrator to manage non-containerized applications." 273 | categories: ["System"] 274 | mani: 275 | identifier: "alajmo/mani" 276 | description: "CLI tool to help you manage repositories" 277 | categories: ["Git"] 278 | mise: 279 | identifier: "jdx/mise" 280 | description: "dev tools, env vars, task runner." 281 | asset_filters: ["^tar", "^musl"] 282 | categories: ["Development"] 283 | macchina: 284 | identifier: "Macchina-CLI/macchina" 285 | description: "A system information frontend with an emphasis on performance. (neofetch alternative)" 286 | categories: ["System"] 287 | github-cli: 288 | identifier: "cli/cli" 289 | description: "GitHub’s official command line tool." 290 | categories: ["Git"] 291 | gitu: 292 | identifier: "altsem/gitu" 293 | description: "A TUI Git client inspired by Magit." 294 | categories: ["Git"] 295 | gstring: 296 | identifier: "Allaman/gstring" 297 | description: "Swiss army knife for manipulating strings." 298 | categories: ["Text"] 299 | difftastic: 300 | identifier: "Wilfred/difftastic" 301 | description: "A structural diff that understands syntax." 302 | categories: ["Git"] 303 | doggo: 304 | identifier: "mr-karan/doggo" 305 | description: "Command-line DNS Client for Humans. Written in Golang." 306 | categories: ["Network"] 307 | serpl: 308 | identifier: "yassinebridi/serpl" 309 | description: "A simple terminal UI for search and replace, ala VS Code." 310 | categories: ["Text"] 311 | svu: 312 | identifier: "caarlos0/svu" 313 | description: "Semantic Version Util." 314 | categories: ["Git"] 315 | dust: 316 | identifier: "bootandy/dust" 317 | description: "A more intuitive version of du in rust." 318 | categories: ["System"] 319 | jnv: 320 | identifier: "ynqa/jnv" 321 | description: "Interactive JSON filter using jq." 322 | categories: ["Json"] 323 | fx: 324 | identifier: "antonmedv/fx" 325 | description: "Terminal JSON viewer & processor" 326 | categories: ["Json"] 327 | humanlog: 328 | identifier: "humanlogio/humanlog" 329 | description: "Logs for humans to read." 330 | categories: ["System"] 331 | f2: 332 | identifier: "ayoisaiah/f2" 333 | description: "F2 is a cross-platform command-line tool for batch renaming files and directories quickly and safely. Written in Go!" 334 | categories: ["Files"] 335 | nerdctl: 336 | identifier: "containerd/nerdctl" 337 | description: "contaiNERD CTL - Docker-compatible CLI for containerd, with support for Compose, Rootless, eStargz, OCIcrypt, IPFS, ..." 338 | categories: ["Docker"] 339 | kaf: 340 | identifier: "birdayz/kaf" 341 | description: "Modern CLI for Apache Kafka, written in Go." 342 | categories: ["Development"] 343 | chiko: 344 | identifier: "felangga/chiko" 345 | description: "The ultimate beauty gRPC Client on your Terminal!" 346 | categories: ["Development"] 347 | go-size-analyzer: 348 | identifier: "Zxilly/go-size-analyzer" 349 | description: "A tool for analyzing the size of compiled Go binaries, offering cross-platform support, detailed breakdowns, and multiple output formats." 350 | file: "gsa" 351 | categories: ["Development"] 352 | gobuster: 353 | identifier: "OJ/gobuster" 354 | description: "Directory/File, DNS and VHost busting tool written in Go." 355 | categories: ["Security"] 356 | naabu: 357 | identifier: "projectdiscovery/naabu" 358 | description: "A fast port scanner written in go with a focus on reliability and simplicity. Designed to be used in combination with other tools for attack surface discovery in bug bounties and pentests." 359 | categories: ["Security"] 360 | httpx: 361 | identifier: "projectdiscovery/httpx" 362 | description: "httpx is a fast and multi-purpose HTTP toolkit that allows running multiple probes using the retryablehttp library." 363 | categories: ["Security"] 364 | dnsx: 365 | identifier: "projectdiscovery/dnsx" 366 | description: "dnsx is a fast and multi-purpose DNS toolkit allow to run multiple DNS queries of your choice with a list of user-supplied resolvers." 367 | categories: ["Security"] 368 | ffuf: 369 | identifier: "ffuf/ffuf" 370 | description: "Fast web fuzzer written in Go." 371 | categories: ["Security"] 372 | katana: 373 | identifier: "projectdiscovery/katana" 374 | description: "A next-generation crawling and spidering framework." 375 | categories: ["Security"] 376 | subfinder: 377 | identifier: "projectdiscovery/subfinder" 378 | description: "Fast passive subdomain enumeration tool." 379 | categories: ["Security"] 380 | rustscan: 381 | identifier: "RustScan/RustScan" 382 | description: "The Modern Port Scanner." 383 | categories: ["Security"] 384 | skim: 385 | identifier: "lotabout/skim" 386 | description: "Fuzzy Finder in rust!" 387 | categories: ["Text"] 388 | tokei: 389 | identifier: "XAMPPRocky/tokei" 390 | description: "Count your code, quickly." 391 | asset_filter: ["^alpha"] 392 | categories: ["Development"] 393 | choose: 394 | identifier: "theryangeary/choose" 395 | description: "A human-friendly and fast alternative to cut and (sometimes) awk." 396 | asset_filter: ["^musl"] 397 | categories: ["Text"] 398 | hyperfine: 399 | identifier: "sharkdp/hyperfine" 400 | description: "A command-line benchmarking tool." 401 | categories: ["Development", "Testing"] 402 | inlyne: 403 | identifier: "Inlyne-Project/inlyne" 404 | description: "Introducing Inlyne, a GPU powered yet browserless tool to help you quickly view markdown files in the blink of an eye." 405 | categories: ["Markdown"] 406 | rargs: 407 | identifier: "lotabout/rargs" 408 | description: "xargs + awk with pattern matching support." 409 | categories: ["Text"] 410 | xh: 411 | identifier: "ducaale/xh" 412 | description: "Friendly and fast tool for sending HTTP requests." 413 | categories: ["Network"] 414 | static-web-server: 415 | identifier: "static-web-server/static-web-server" 416 | description: "A cross-platform, high-performance and asynchronous web server for static files-serving." 417 | categories: ["Network"] 418 | just: 419 | identifier: "casey/just" 420 | description: "Just a command runner." 421 | categories: ["Shell"] 422 | eza: 423 | identifier: "eza-community/eza" 424 | description: "A modern, maintained replacement for ls." 425 | categories: ["Files"] 426 | coreutils: 427 | identifier: "uutils/coreutils" 428 | description: "Cross-platform Rust rewrite of the GNU coreutils." 429 | categories: ["System"] 430 | funzzy: 431 | identifier: "cristianoliveira/funzzy" 432 | description: "A lightweight blazingly fast file watcher." 433 | categories: ["Development", "Files"] 434 | watchexec: 435 | identifier: "watchexec/watchexec" 436 | description: "Executes commands in response to file modifications." 437 | categories: ["Development", "Files"] 438 | kubie: 439 | identifier: "sbstp/kubie" 440 | description: "A more powerful alternative to kubectx and kubens." 441 | categories: ["Kubernetes"] 442 | ngtop: 443 | identifier: "facundoolano/ngtop" 444 | description: "Request analytics from the nginx access logs." 445 | categories: ["Network", "Security"] 446 | dasel: 447 | identifier: "TomWright/dasel" 448 | description: "Select, put and delete data from JSON, TOML, YAML, XML and CSV files with a single tool. Supports conversion between formats and can be used as a Go package." 449 | asset_filters: [".gz"] 450 | categories: ["Json", "Yaml"] 451 | pug: 452 | identifier: "leg100/pug" 453 | description: "Drive terraform at terminal velocity." 454 | categories: ["Cloud"] 455 | binsider: 456 | identifier: "orhun/binsider" 457 | description: "Analyze ELF binaries like a boss." 458 | categories: ["Security"] 459 | asset_filters: ["^sig", "^sha512", ".tar.gz"] 460 | gtree: 461 | identifier: "ddddddO/gtree" 462 | description: "Using either Markdown or Programmatically to generate trees and directories, and to verify directories. Provide CLI, Golang library and Web." 463 | categories: ["Markdown", "Text"] 464 | pipet: 465 | identifier: "bjesus/pipet" 466 | description: "A swiss-army tool for scraping and extracting data from online assets, made for hackers." 467 | categories: ["Network"] 468 | rpn: 469 | identifier: "marcopaganini/rpn" 470 | description: "A CLI RPN calculator in Go." 471 | categories: ["System"] 472 | up: 473 | identifier: "akavel/up" 474 | description: "Ultimate Plumber is a tool for writing Linux pipes with instant live preview." 475 | categories: ["System"] 476 | up-net: 477 | identifier: "jesusprubio/up" 478 | description: "Troubleshoot problems with your Internet connection." 479 | categories: ["Network"] 480 | usql: 481 | identifier: "xo/usql" 482 | description: "Universal command-line interface for SQL databases." 483 | categories: ["Development", "Database"] 484 | chezmoi: 485 | identifier: "twpayne/chezmoi" 486 | description: "Manage your dotfiles across multiple diverse machines, securely." 487 | categories: ["Development"] 488 | asset_filters: [".tar.gz"] 489 | jqp: 490 | identifier: "noahgorstein/jqp" 491 | description: "A TUI playground to experiment with jq." 492 | categories: ["Json"] 493 | television: 494 | identifier: "alexpasmantier/television" 495 | description: "Television is a blazingly fast general purpose fuzzy finder TUI." 496 | categories: ["Text"] 497 | onefetch: 498 | identifier: "o2sh/onefetch" 499 | description: "Command-line Git information tool." 500 | categories: ["Git"] 501 | repgrep: 502 | identifier: "acheronfail/repgrep" 503 | description: "An interactive replacer for ripgrep that makes it easy to find and replace across files on the command line." 504 | categories: ["Text"] 505 | serie: 506 | identifier: "lusingander/serie" 507 | description: "A rich git commit graph in your terminal, like magic." 508 | categories: ["Git"] 509 | rainfrog: 510 | identifier: "achristmascarl/rainfrog" 511 | description: "A database management tui for postgres, mysql, and sqlite." 512 | categories: ["Development", "Database"] 513 | atac: 514 | identifier: "Julien-cpsn/ATAC" 515 | description: "A simple API client (postman like) in your terminal." 516 | categories: ["Development", "Network"] 517 | presenterm: 518 | identifier: "mfontanini/presenterm" 519 | description: "A markdown terminal slideshow tool." 520 | categories: ["Markdown"] 521 | freeze: 522 | identifier: "charmbracelet/freeze" 523 | description: "Generate images of code and terminal output 📸." 524 | categories: ["Shell"] 525 | asset_filters: ["^sbom"] 526 | mods: 527 | identifier: "charmbracelet/mods" 528 | description: "AI on the command line." 529 | categories: ["AI"] 530 | asset_filters: ["^sbom"] 531 | gum: 532 | identifier: "charmbracelet/gum" 533 | description: "A tool for glamorous shell scripts 🎀." 534 | categories: ["Shell"] 535 | asset_filters: ["^sbom"] 536 | aichat: 537 | identifier: "sigoden/aichat" 538 | description: "All-in-one LLM CLI tool featuring Shell Assistant, Chat-REPL, RAG, AI tools & agents, with access to OpenAI, Claude, Gemini, Ollama, Groq, and more." 539 | categories: ["AI"] 540 | znscli: 541 | identifier: "znscli/zns" 542 | description: "CLI tool for querying DNS records with readable, colored output." 543 | categories: ["Network"] 544 | superfile: 545 | identifier: "yorukot/superfile" 546 | description: "Pretty fancy and modern terminal file manager." 547 | categories: ["Files"] 548 | ghorg: 549 | identifier: "gabrie30/ghorg" 550 | description: "Quickly clone or backup an entire org/users repositories into one directory - Supports GitHub, GitLab, Bitbucket, and more 🐇🥚" 551 | categories: ["Git"] 552 | stu: 553 | identifier: "lusingander/stu" 554 | description: "TUI explorer application for Amazon S3 (AWS S3) 🪣" 555 | categories: ["Cloud"] 556 | t-rec: 557 | identifier: "sassman/t-rec-rs" 558 | description: "Blazingly fast terminal recorder that generates animated gif images for the web written in rust." 559 | file: "t-rec" 560 | categories: ["Development"] 561 | numbat: 562 | identifier: "sharkdp/numbat" 563 | description: "A statically typed programming language for scientific computations with first class support for physical dimensions and units." 564 | categories: ["Math"] 565 | kubectl: 566 | # TODO: version handling 567 | identifier: "https://dl.k8s.io/release/v1.27.16/bin/OSNAME/ARCH/kubectl" 568 | description: "The Kubernetes command-line tool, kubectl, allows you to run commands against Kubernetes clusters." 569 | file: "kubectl" 570 | hugo: 571 | identifier: "gohugoio/hugo" 572 | description: "The world’s fastest framework for building websites." 573 | categories: ["Network"] 574 | asset_filters: ["hugo_extended", "^deploy"] 575 | pet: 576 | identifier: "knqyf263/pet" 577 | description: "Simple command-line snippet manager." 578 | categories: ["Text"] 579 | telepresence: 580 | identifier: "telepresenceio/telepresence" 581 | description: "Local development against a remote Kubernetes or OpenShift cluster." 582 | categories: ["Network"] 583 | gecho: 584 | identifier: "allaman/gecho" 585 | description: "A simple HTTP/TCP server." 586 | categories: ["Network"] 587 | system-manager-tui: 588 | identifier: "matheus-git/systemd-manager-tui" 589 | description: "A program for managing systemd services through a TUI (Terminal User Interfaces)." 590 | categories: ["System"] 591 | asset_filters: ["^deb"] 592 | xan: 593 | identifier: "medialab/xan" 594 | description: "The CSV magician." 595 | categories: ["CSV"] 596 | qsv: 597 | identifier: "dathere/qsv" 598 | description: "Blazing-fast Data-Wrangling toolkit." 599 | categories: ["CSV", "Database", "Json"] 600 | caddy: 601 | identifier: "caddyserver/caddy" 602 | description: "Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS" 603 | categories: ["Network"] 604 | asset_filters: ["tar.gz", "^sig"] 605 | crush: 606 | identifier: "charmbracelet/crush" 607 | description: "The glamourous AI coding agent for your favourite terminal." 608 | categories: ["AI"] 609 | asset_filters: ["^sbom"] 610 | schema: 611 | identifier: "gigagrug/schema" 612 | description: "A CLI tool for working with the database | SQLite, libSQL, PostgreSQL, MySQL, MariaDB" 613 | categories: ["Development", "Database"] 614 | gonzo: 615 | identifier: "control-theory/gonzo" 616 | description: "Gonzo! The Go based TUI log analysis tool" 617 | categories: ["System"] 618 | wrkflw: 619 | identifier: "bahdotsh/wrkflw" 620 | description: "Validate and Run GitHub Actions locally." 621 | categories: ["Development"] 622 | kat: 623 | identifier: "MacroPower/kat" 624 | description: "TUI and rule-based rendering engine for Kubernetes manifests" 625 | categories: ["Kubernetes", "GitOps"] 626 | asset_filters: ["tar.gz", "^sig", "^json"] 627 | onedump: 628 | identifier: "liweiyi88/onedump" 629 | description: "Effortless database administration tool" 630 | categories: ["Database"] 631 | jj: 632 | identifier: "jj-vcs/jj" 633 | description: "A Git-compatible VCS that is both simple and powerful" 634 | categories: ["Development", "Git"] 635 | jjui: 636 | identifier: "idursun/jjui" 637 | description: "Jujutsu UI (jjui) is a Text User Interface (TUI) designed for interacting with the Jujutsu version control system." 638 | categories: ["Development", "Git"] 639 | yozefu: 640 | identifier: "MAIF/yozefu" 641 | description: "An interactive terminal user interface (TUI) application for exploring data of a kafka cluster." 642 | categories: ["Development"] 643 | asset_filters: ["^json"] 644 | oha: 645 | identifier: "hatoo/oha" 646 | description: "Ohayou(おはよう), HTTP load generator, inspired by rakyll/hey with tui animation." 647 | categories: ["Development", "Testing"] 648 | --------------------------------------------------------------------------------