├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Conveniently download your favorite binaries (currently 152 supported)!
13 |
14 |
15 | 
16 |
17 |
18 | Open a tool's README within werkzeugkasten
19 |
20 | 
21 |
22 |
23 |
24 |
25 | Install a specific version
26 |
27 | 
28 |
29 |
30 |
31 |
32 | List categories and tool count
33 |
34 | `werkzeugkasten -categories`
35 |
36 | 
37 |
38 |
39 |
40 |
41 | List tools in category "Text"
42 |
43 | `werkzeugkasten -category text`
44 |
45 | 
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 | 
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 |
--------------------------------------------------------------------------------