diff --git a/Makefile b/Makefile deleted file mode 100644 index 27b98cb..0000000 --- a/Makefile +++ /dev/null @@ -1,22 +0,0 @@ -BINARY_NAME=qube -VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null || echo "dev") -MAIN=. -BUILD_DIR=./bin - -.PHONY: build clean - -build: - @echo "🔧 Building $(BINARY_NAME) version $(VERSION)" - go build \ - -ldflags="-X github.com/apiqube/cli/cmd.version=$(VERSION)" \ - -o $(BUILD_DIR)/$(BINARY_NAME)-$(VERSION).exe $(MAIN) - -clean: - @echo "🧹 Cleaning..." - rm -rf $(BUILD_DIR) - -go-fmt: - gofumpt -l -w . - -go-lint: - golangci-lint run ./... \ No newline at end of file diff --git a/Taskfile.yml b/Taskfile.yml index d147afd..9b37a98 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -38,4 +38,14 @@ tasks: dev: desc: 👨‍💻 Watch mode (requires reflex or air) cmds: - - reflex -r '\.go$$' -s -- sh -c "task build && task run" \ No newline at end of file + - reflex -r '\.go$$' -s -- sh -c "task build && task run" + + go-fmt: + desc: 🧹 Cleaning all go code + cmds: + - gofumpt -l -w . + + go-lint: + desc: 🚀 Command for linting code + cmds: + - golangci-lint run ./... \ No newline at end of file diff --git a/cmd/cli/apply/apply.go b/cmd/cli/apply/apply.go index 94f45e9..2cf4638 100644 --- a/cmd/cli/apply/apply.go +++ b/cmd/cli/apply/apply.go @@ -4,7 +4,7 @@ import ( "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/loader" "github.com/apiqube/cli/internal/core/store" - "github.com/apiqube/cli/ui" + "github.com/apiqube/cli/ui/cli" "github.com/spf13/cobra" ) @@ -20,39 +20,37 @@ var Cmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { file, err := cmd.Flags().GetString("file") if err != nil { - ui.Errorf("Failed to parse --file: %s", err.Error()) + cli.Errorf("Failed to parse --file: %s", err.Error()) return } - ui.Printf("Loading manifests from: %s", file) - ui.Spinner(true, "Applying manifests...") - defer ui.Spinner(false) + cli.Infof("Loading manifests from: %s", file) loadedMans, cachedMans, err := loader.LoadManifests(file) if err != nil { - ui.Errorf("Failed to load manifests: %s", err.Error()) + cli.Errorf("Failed to load manifests: %s", err.Error()) return } printManifestsLoadResult(loadedMans, cachedMans) if err := store.Save(loadedMans...); err != nil { - ui.Error("Failed to save manifests: " + err.Error()) + cli.Infof("Failed to save manifests: %s", err.Error()) return } - ui.Println("Manifests applied successfully") + cli.Success("Manifests applied successfully") }, } func printManifestsLoadResult(newMans, cachedMans []manifests.Manifest) { - ui.Infof("Loaded %d new manifests", len(newMans)) - for _, m := range newMans { - ui.Infof("New manifest added: %s (h: %s...)", m.GetID(), ui.ShortHash(m.GetMeta().GetHash())) + cli.Infof("New manifest added: %s (h: %s...)", m.GetID(), cli.ShortHash(m.GetMeta().GetHash())) } for _, m := range cachedMans { - ui.Infof("Manifest %s unchanged (h: %s...) - using cached version", m.GetID(), ui.ShortHash(m.GetMeta().GetHash())) + cli.Infof("Manifest %s unchanged (h: %s...) - using cached version", m.GetID(), cli.ShortHash(m.GetMeta().GetHash())) } + + cli.Infof("Loaded new manifests\nNew: %d\nCached: %d", len(newMans), len(cachedMans)) } diff --git a/cmd/cli/check/check.go b/cmd/cli/check/check.go index 3bdabd6..be45204 100644 --- a/cmd/cli/check/check.go +++ b/cmd/cli/check/check.go @@ -4,12 +4,13 @@ import ( "fmt" "strings" + "github.com/apiqube/cli/ui/cli" + "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/kinds/plan" "github.com/apiqube/cli/internal/core/manifests/loader" runner "github.com/apiqube/cli/internal/core/runner/plan" "github.com/apiqube/cli/internal/core/store" - "github.com/apiqube/cli/ui" "github.com/spf13/cobra" ) @@ -39,9 +40,6 @@ var cmdPlanCheck = &cobra.Command{ return uiErrorf("%s", err.Error()) } - ui.Spinner(true, "Checking manifests...") - defer ui.Spinner(false) - loadedManifests, err := loadManifests(opts) if err != nil { return uiErrorf("Failed to load manifests: %v", err) @@ -56,7 +54,7 @@ var cmdPlanCheck = &cobra.Command{ return uiErrorf("Failed to check plan: %v", err) } - ui.Successf("Successfully checked plan manifest") + cli.Successf("Successfully checked plan manifest") return nil }, } @@ -100,7 +98,7 @@ type ( ) func uiErrorf(format string, args ...interface{}) error { - ui.Errorf(format, args...) + cli.Errorf(format, args...) return nil } @@ -124,7 +122,7 @@ func loadManifests(opts *checkPlanOptions) ([]manifests.Manifest, error) { case opts.flagsSet["file"]: loadedMans, _, err := loader.LoadManifests(opts.file) if err == nil { - ui.Infof("Manifests from provided path %s loaded", opts.file) + cli.Infof("Manifests from provided path %s loaded", opts.file) } return loadedMans, err diff --git a/cmd/cli/cleanup/cleanup.go b/cmd/cli/cleanup/cleanup.go index 6f7b985..1d72530 100644 --- a/cmd/cli/cleanup/cleanup.go +++ b/cmd/cli/cleanup/cleanup.go @@ -3,8 +3,9 @@ package cleanup import ( "fmt" + "github.com/apiqube/cli/ui/cli" + "github.com/apiqube/cli/internal/core/store" - "github.com/apiqube/cli/ui" "github.com/spf13/cobra" ) @@ -29,14 +30,11 @@ var Cmd = &cobra.Command{ keep = keepVersionDefault } - ui.Spinner(true, "Cleaning up...") - defer ui.Spinner(false) - if err = store.CleanupOldVersions(opts.manifestID, keep); err != nil { - ui.Errorf("Failed to cleanup old versions: %v", err) + cli.Errorf("Failed to cleanup old versions: %v", err) } - ui.Successf("Successfully cleaned up %v to last %d versions", opts.manifestID, keep) + cli.Successf("Successfully cleaned up %v to last %d versions", opts.manifestID, keep) return nil }, } diff --git a/cmd/cli/rollback/rollback.go b/cmd/cli/rollback/rollback.go index 16fee82..2b729a0 100644 --- a/cmd/cli/rollback/rollback.go +++ b/cmd/cli/rollback/rollback.go @@ -3,8 +3,9 @@ package rollback import ( "fmt" + "github.com/apiqube/cli/ui/cli" + "github.com/apiqube/cli/internal/core/store" - "github.com/apiqube/cli/ui" "github.com/spf13/cobra" ) @@ -25,15 +26,12 @@ var Cmd = &cobra.Command{ targetVersion = 1 } - ui.Spinner(true, "Rolling back") - defer ui.Spinner(false) - if err = store.Rollback(opts.manifestID, targetVersion); err != nil { - ui.Errorf("Error rolling back to previous version: %s", err) + cli.Errorf("Error rolling back to previous version: %s", err) return } - ui.Successf("Successfully rolled back %s to version %d\n", opts.manifestID, targetVersion) + cli.Successf("Successfully rolled back %s to version %d\n", opts.manifestID, targetVersion) }, } diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 5a28358..279cd06 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -2,7 +2,11 @@ package cli import ( "context" - "fmt" + "os" + "os/signal" + "syscall" + + "github.com/apiqube/cli/ui/cli" "github.com/apiqube/cli/cmd/cli/apply" "github.com/apiqube/cli/cmd/cli/check" @@ -22,14 +26,13 @@ var configKey contextKey = "config" var rootCmd = &cobra.Command{ Use: "qube", Short: "ApiQube is a powerful test manager for APIs", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + PersistentPreRun: func(cmd *cobra.Command, args []string) { cfg, err := config.InitConfig() if err != nil { - return fmt.Errorf("config init failed: %w", err) + cli.Errorf("Error initializing config: %s", err.Error()) } - cmd.SetContext(context.WithValue(cmd.Context(), configKey, cfg)) - return nil + cmd.SetContext(configureContext(context.WithValue(cmd.Context(), configKey, cfg))) }, } @@ -45,3 +48,14 @@ func Execute() { cobra.CheckErr(rootCmd.Execute()) } + +func configureContext(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + go func() { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) + <-sigCh + cancel() + }() + return ctx +} diff --git a/cmd/cli/search/results.go b/cmd/cli/search/results.go index a2da169..934d5bd 100644 --- a/cmd/cli/search/results.go +++ b/cmd/cli/search/results.go @@ -7,10 +7,9 @@ import ( "path/filepath" "sort" "strings" - "time" "github.com/apiqube/cli/internal/core/manifests" - "github.com/apiqube/cli/ui" + "github.com/apiqube/cli/ui/cli" "gopkg.in/yaml.v3" ) @@ -82,13 +81,13 @@ func displayResults(manifests []manifests.Manifest) { headers := []string{ "#", "Hash", - "kind", - "name", - "namespace", - "version", + "Kind", + "Name", + "Namespace", + "Version", "Created", "Updated", - "Last Updated", + "Last Applied", } var rows [][]string @@ -96,31 +95,29 @@ func displayResults(manifests []manifests.Manifest) { meta := m.GetMeta() row := []string{ fmt.Sprint(i + 1), - ui.ShortHash(meta.GetHash()), + cli.ShortHash(meta.GetHash()), m.GetKind(), m.GetName(), m.GetNamespace(), fmt.Sprint(meta.GetVersion()), - meta.GetCreatedAt().Format(time.RFC3339), - meta.GetUpdatedAt().Format(time.RFC3339), - meta.GetLastApplied().Format(time.RFC3339), + meta.GetCreatedAt().Format("2006-01-02 15:04:05"), + meta.GetUpdatedAt().Format("2006-01-02 15:04:05"), + meta.GetLastApplied().Format("2006-01-02 15:04:05"), } rows = append(rows, row) } - ui.Table(headers, rows) + cli.Table(headers, rows) } func handleSearchResults(manifests []manifests.Manifest, opts *Options) error { - ui.Infof("Found %d manifests", len(manifests)) + cli.Success("Search completed") + cli.Infof("Found %d manifests", len(manifests)) if len(opts.sortBy) > 0 { sortManifests(manifests, opts.sortBy) } - ui.Spinner(true, "Preparing results...") - defer ui.Spinner(false) - if opts.output { if err := outputResults(manifests, opts); err != nil { return fmt.Errorf("output failed: %w", err) @@ -129,7 +126,6 @@ func handleSearchResults(manifests []manifests.Manifest, opts *Options) error { displayResults(manifests) } - ui.Success("Search completed") return nil } @@ -206,7 +202,7 @@ func ensureOutputDirectory(path string) error { } if _, err := os.Stat(path); os.IsNotExist(err) { - ui.Infof("Creating output directory: %s", path) + cli.Infof("Creating output directory: %s", path) if err = os.MkdirAll(path, 0o755); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } @@ -221,7 +217,7 @@ func writeSeparateOutputs(manifests []manifests.Manifest, opts *Options) error { return fmt.Errorf("failed to write manifest %s: %w", m.GetID(), err) } } - ui.Successf("Successfully wrote %d manifests to %s", len(manifests), opts.outputPath) + cli.Successf("Successfully wrote %d manifests to %s", len(manifests), opts.outputPath) return nil } diff --git a/cmd/cli/search/search.go b/cmd/cli/search/search.go index 08acac3..dbbe506 100644 --- a/cmd/cli/search/search.go +++ b/cmd/cli/search/search.go @@ -3,7 +3,7 @@ package search import ( "fmt" - "github.com/apiqube/cli/ui" + "github.com/apiqube/cli/ui/cli" "github.com/spf13/cobra" ) @@ -30,7 +30,7 @@ time ranges, and output formatting`, } if len(manifests) == 0 { - ui.Warning("No manifests found matching the criteria") + cli.Warning("No manifests found matching the criteria") return nil } diff --git a/cmd/qube/main.go b/cmd/qube/main.go index 0f35813..5bf9e04 100644 --- a/cmd/qube/main.go +++ b/cmd/qube/main.go @@ -1,21 +1,17 @@ package main import ( - "time" - "github.com/apiqube/cli/cmd/cli" "github.com/apiqube/cli/internal/core/store" - "github.com/apiqube/cli/ui" + uicli "github.com/apiqube/cli/ui/cli" ) func main() { - ui.Init() - defer ui.Stop() + uicli.Init() + defer uicli.Stop() store.Init() defer store.Stop() cli.Execute() - - time.Sleep(time.Second) } diff --git a/go.mod b/go.mod index acc1771..99da1ae 100644 --- a/go.mod +++ b/go.mod @@ -5,16 +5,19 @@ go 1.24.3 require ( github.com/adrg/xdg v0.5.3 github.com/blevesearch/bleve/v2 v2.5.1 - github.com/charmbracelet/bubbletea v1.3.5 github.com/charmbracelet/lipgloss v1.1.0 github.com/dgraph-io/badger/v4 v4.7.0 github.com/google/uuid v1.6.0 + github.com/pterm/pterm v0.12.80 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 gopkg.in/yaml.v3 v3.0.1 ) require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bits-and-blooms/bitset v1.22.0 // indirect @@ -40,9 +43,9 @@ require ( github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/containerd/console v1.0.3 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -50,16 +53,15 @@ require ( github.com/golang/protobuf v1.5.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/gookit/color v1.5.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mschoch/smat v0.2.0 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -80,6 +82,7 @@ require ( golang.org/x/net v0.38.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 6e4dfd4..dc7a197 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,25 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= @@ -45,8 +63,6 @@ github.com/blevesearch/zapx/v16 v16.2.3 h1:7Y0r+a3diEvlazsncexq1qoFOcBd64xwMS7aD github.com/blevesearch/zapx/v16 v16.2.3/go.mod h1:wVJ+GtURAaRG9KQAMNYyklq0egV+XJlGcXNCE0OFjjA= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= -github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= @@ -57,6 +73,8 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -69,8 +87,6 @@ github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa5 github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= @@ -95,36 +111,54 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede h1:YrgBGwxMRK0Vq0WSCWFaZUnTsrA/PZE/xs1QZh+/edg= github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.80 h1:mM55B+GnKUnLMUSqhdINe4s6tOuVQIetQ3my8JGyAIg= +github.com/pterm/pterm v0.12.80/go.mod h1:c6DeF9bSnOSeFPZlfs4ZRAFcf5SCoTwvwQ5xaKGQlHo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -133,6 +167,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -147,13 +183,17 @@ github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -168,27 +208,67 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/core/manifests/kinds/plan/plan.go b/internal/core/manifests/kinds/plan/plan.go index 75da5bb..db7e2f1 100644 --- a/internal/core/manifests/kinds/plan/plan.go +++ b/internal/core/manifests/kinds/plan/plan.go @@ -24,7 +24,7 @@ type Plan struct { Spec struct { Stages []Stage `yaml:"stages" json:"stages"` - Hooks Hooks `yaml:"hooks,omitempty" json:"hooks,omitempty"` + Hooks *Hooks `yaml:"hooks,omitempty" json:"hooks,omitempty"` } `yaml:"spec" json:"spec"` Meta *kinds.Meta `yaml:",inline" json:"meta"` diff --git a/internal/core/manifests/kinds/tests/api/http.go b/internal/core/manifests/kinds/tests/api/http.go index 0d6d3c9..a59fabe 100644 --- a/internal/core/manifests/kinds/tests/api/http.go +++ b/internal/core/manifests/kinds/tests/api/http.go @@ -23,7 +23,7 @@ type Http struct { kinds.BaseManifest `yaml:",inline" json:",inline"` Spec struct { - Server string `yaml:"server,omitempty" json:"server,omitempty"` + Target string `yaml:"target,omitempty" json:"target,omitempty"` Cases []HttpCase `yaml:"cases" valid:"required,length(1|100)" json:"cases"` } `yaml:"spec" json:"spec" valid:"required"` diff --git a/internal/core/manifests/kinds/tests/base.go b/internal/core/manifests/kinds/tests/base.go index 8381581..0373db5 100644 --- a/internal/core/manifests/kinds/tests/base.go +++ b/internal/core/manifests/kinds/tests/base.go @@ -11,15 +11,15 @@ type HttpCase struct { Url string `yaml:"url,omitempty" json:"url,omitempty"` Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"` Body map[string]any `yaml:"body,omitempty" json:"body,omitempty"` - Assert Assert `yaml:"assert,omitempty" json:"assert,omitempty"` - Save Save `yaml:"save,omitempty" json:"save,omitempty"` - Pass Pass `yaml:"pass,omitempty" json:"pass,omitempty"` + Assert *Assert `yaml:"assert,omitempty" json:"assert,omitempty"` + Save *Save `yaml:"save,omitempty" json:"save,omitempty"` + Pass []Pass `yaml:"pass,omitempty" json:"pass,omitempty"` Timeout time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"` Parallel bool `yaml:"async,omitempty" json:"async,omitempty"` } type Assert struct { - Assertions []*AssertElement `yaml:",inline,omitempty" json:",inline,omitempty"` + Assertions []AssertElement `yaml:",inline,omitempty" json:",inline,omitempty"` } type AssertElement struct { diff --git a/internal/core/manifests/loader/loader.go b/internal/core/manifests/loader/loader.go index 6c5baae..4e6fb4c 100644 --- a/internal/core/manifests/loader/loader.go +++ b/internal/core/manifests/loader/loader.go @@ -8,14 +8,14 @@ import ( "strings" "time" + "github.com/apiqube/cli/ui/cli" + "gopkg.in/yaml.v3" + "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/manifests/hash" "github.com/apiqube/cli/internal/core/manifests/parsing" "github.com/apiqube/cli/internal/core/store" - "github.com/apiqube/cli/ui" - - "github.com/apiqube/cli/internal/core/manifests" ) func LoadManifests(path string) (new []manifests.Manifest, cached []manifests.Manifest, err error) { @@ -109,7 +109,7 @@ func processFile(filePath string, manifestsSet map[string]struct{}) (new []manif } if _, exists := manifestsSet[manifestID]; exists { - ui.Warningf("Duplicate manifest ID: %s (from %s)", manifestID, filepath.Base(filePath)) + cli.Warningf("Duplicate manifest ID: %s (from %s)", manifestID, filepath.Base(filePath)) continue } diff --git a/internal/core/runner/cli/output.go b/internal/core/runner/cli/output.go index c9808ea..ccb4b78 100644 --- a/internal/core/runner/cli/output.go +++ b/internal/core/runner/cli/output.go @@ -4,9 +4,10 @@ import ( "fmt" "strings" + "github.com/apiqube/cli/ui/cli" + "github.com/apiqube/cli/internal/core/manifests" "github.com/apiqube/cli/internal/core/runner/interfaces" - "github.com/apiqube/cli/ui" ) var _ interfaces.Output = (*Output)(nil) @@ -18,12 +19,12 @@ func NewOutput() *Output { } func (o *Output) StartCase(manifest manifests.Manifest, caseName string) { - ui.Infof("Start %s case from %s manifest", caseName, manifest.GetName()) + cli.Infof("Start %s case from %s manifest", caseName, manifest.GetName()) } func (o *Output) EndCase(manifest manifests.Manifest, caseName string, result *interfaces.CaseResult) { if result != nil { - ui.Println(fmt.Sprintf( + cli.Println(fmt.Sprintf( `Finish %s case from %s manifest with next reults Result: %s Success: %v @@ -38,41 +39,41 @@ func (o *Output) EndCase(manifest manifests.Manifest, caseName string, result *i ), ) } else { - ui.Infof("Finish %s case from %s manifest", caseName, manifest.GetName()) + cli.Infof("Finish %s case from %s manifest", caseName, manifest.GetName()) } } func (o *Output) ReceiveMsg(msg any) { - ui.Infof("Receiving message %s", msg) + cli.Infof("Receiving message %s", msg) } func (o *Output) Log(level interfaces.LogLevel, msg string) { switch level { case interfaces.DebugLevel: - ui.Debug(msg) + cli.Debug(msg) case interfaces.InfoLevel: - ui.Info(msg) + cli.Info(msg) case interfaces.WarnLevel: - ui.Warning(msg) + cli.Warning(msg) case interfaces.ErrorLevel: - ui.Error(msg) + cli.Error(msg) default: - ui.Info(msg) + cli.Info(msg) } } func (o *Output) Logf(level interfaces.LogLevel, format string, args ...any) { switch level { case interfaces.DebugLevel: - ui.Debugf(format, args...) + cli.Debugf(format, args...) case interfaces.InfoLevel: - ui.Infof(format, args...) + cli.Infof(format, args...) case interfaces.WarnLevel: - ui.Warningf(format, args...) + cli.Warningf(format, args...) case interfaces.ErrorLevel: - ui.Errorf(format, args...) + cli.Errorf(format, args...) default: - ui.Infof(format, args...) + cli.Infof(format, args...) } } @@ -83,10 +84,10 @@ func (o *Output) DumpValues(values map[string]any) { rows = append(rows, fmt.Sprintf("%v: %v", k, v)) } - ui.Printf("Damping values: \n%s", strings.Join(rows, "\n")) + cli.Printf("Damping values: \n%s", strings.Join(rows, "\n")) } } func (o *Output) Error(err error) { - ui.Error(err.Error()) + cli.Error(err.Error()) } diff --git a/internal/core/store/independ.go b/internal/core/store/independ.go index d941fec..65f406b 100644 --- a/internal/core/store/independ.go +++ b/internal/core/store/independ.go @@ -4,8 +4,9 @@ import ( "errors" "sync" + "github.com/apiqube/cli/ui/cli" + "github.com/apiqube/cli/internal/core/manifests" - "github.com/apiqube/cli/ui" ) var errStoreNotInitialized = errors.New("store not initialized") @@ -20,7 +21,7 @@ func Init() { once.Do(func() { db, err := NewStorage() if err != nil { - ui.Errorf("Error initializing storage: %v", err) + cli.Errorf("Error initializing storage: %v", err) } instance = db @@ -34,11 +35,11 @@ func Stop() { enabled = false initialized = false if err := instance.db.Close(); err != nil { - ui.Errorf("Failed to close database: %v", err) + cli.Errorf("Failed to close database: %v", err) } if err := instance.index.Close(); err != nil { - ui.Errorf("Failed to close index: %v", err) + cli.Errorf("Failed to close index: %v", err) } instance = nil diff --git a/ui/cli/console.go b/ui/cli/console.go new file mode 100644 index 0000000..d5b28ac --- /dev/null +++ b/ui/cli/console.go @@ -0,0 +1,103 @@ +package cli + +import ( + "fmt" + + "github.com/apiqube/cli/ui" +) + +func Print(msg string) { + if inEnabled() { + instance.Log(ui.TypeLog, msg) + } +} + +func Printf(format string, a ...any) { + if inEnabled() { + instance.Logf(ui.TypeLog, format, a...) + } +} + +func Println(a ...any) { + if inEnabled() { + instance.Log(ui.TypeLog, fmt.Sprintln(a...)) + } +} + +func Debug(msg string) { + if inEnabled() { + instance.Log(ui.TypeDebug, msg) + } +} + +func Debugf(format string, a ...any) { + if inEnabled() { + instance.Logf(ui.TypeDebug, format, a...) + } +} + +func Info(msg string) { + if inEnabled() { + instance.Log(ui.TypeInfo, msg) + } +} + +func Infof(format string, a ...any) { + if inEnabled() { + instance.Logf(ui.TypeInfo, format, a...) + } +} + +func Warning(msg string) { + if inEnabled() { + instance.Log(ui.TypeWarning, msg) + } +} + +func Warningf(format string, a ...any) { + if inEnabled() { + instance.Logf(ui.TypeWarning, format, a...) + } +} + +func Error(msg string) { + if inEnabled() { + instance.Log(ui.TypeError, msg) + } +} + +func Errorf(format string, a ...any) { + if inEnabled() { + instance.Logf(ui.TypeError, format, a...) + } +} + +func Fatal(msg string) { + if inEnabled() { + instance.Log(ui.TypeFatal, msg) + } +} + +func Fatalf(format string, a ...any) { + if inEnabled() { + instance.Logf(ui.TypeFatal, format, a...) + } +} + +func Success(msg string) { + if inEnabled() { + instance.Log(ui.TypeSuccess, msg) + } +} + +func Successf(format string, a ...any) { + if inEnabled() { + instance.Logf(ui.TypeSuccess, format, a...) + } +} + +func Done(msg string) { + if inEnabled() { + instance.Done(msg) + } +} diff --git a/ui/helpers.go b/ui/cli/helpers.go similarity index 91% rename from ui/helpers.go rename to ui/cli/helpers.go index dc9e124..c77c75b 100644 --- a/ui/helpers.go +++ b/ui/cli/helpers.go @@ -1,4 +1,4 @@ -package ui +package cli func ShortHash(hash string) string { return hash[:8] diff --git a/ui/cli/independ.go b/ui/cli/independ.go new file mode 100644 index 0000000..fd78b98 --- /dev/null +++ b/ui/cli/independ.go @@ -0,0 +1,60 @@ +package cli + +import ( + "sync" + + "github.com/apiqube/cli/ui" +) + +var ( + instance *UI + once sync.Once + enabled bool +) + +func Init() { + once.Do(func() { + instance = NewUI() + enabled = true + }) +} + +func Stop() { + if instance != nil && enabled { + enabled = false + instance = nil + } +} + +func inEnabled() bool { + return instance != nil && enabled +} + +func Table(headers []string, rows [][]string) { + if inEnabled() { + instance.Table(headers, rows) + } +} + +func Progress() ui.ProgressReporter { + if inEnabled() { + return instance.Progress() + } + + return &progressReporter{} +} + +func Spinner() ui.SpinnerReporter { + if inEnabled() { + return instance.Spinner() + } + + return &spinnerReporter{} +} + +func Snippet() ui.SnippetReporter { + if inEnabled() { + return instance.Snippet() + } + return &snippetReporter{} +} diff --git a/ui/cli/logger.go b/ui/cli/logger.go new file mode 100644 index 0000000..8b01525 --- /dev/null +++ b/ui/cli/logger.go @@ -0,0 +1,71 @@ +package cli + +import ( + "fmt" + "strings" + "time" + + "github.com/apiqube/cli/ui" + "github.com/charmbracelet/lipgloss" +) + +func (u *UI) Log(level ui.LogLevel, msg string) { + fmt.Print(formatLog(level, msg)) +} + +func (u *UI) Logf(level ui.LogLevel, format string, args ...any) { + u.Log(level, fmt.Sprintf(format, args...)) +} + +func (u *UI) Error(err error) { + u.Log(ui.TypeError, err.Error()) +} + +func (u *UI) Done(msg string) { + u.Log(ui.TypeSuccess, msg) +} + +func formatLog(level ui.LogLevel, msg string) string { + var levelText string + var style lipgloss.Style + + switch level { + case ui.TypeDebug: + levelText = "DEBUG" + style = debugStyle + case ui.TypeInfo: + levelText = "INFO" + style = infoStyle + case ui.TypeWarning: + levelText = "WARN" + style = warningStyle + case ui.TypeError: + levelText = "ERROR" + style = errorStyle + case ui.TypeFatal: + levelText = "FATAL" + style = fatalStyle + case ui.TypeSuccess: + levelText = "SUCCESS" + style = successStyle + default: + levelText = "INFO" + style = infoStyle + } + + paddedLevel := fmt.Sprintf("%-7s", levelText) + levelStyled := style.Render(paddedLevel) + + lines := strings.Split(msg, "\n") + baseIndent := len("14:30:45") + 1 + 7 + 2 + + for i := 1; i < len(lines); i++ { + lines[i] = strings.Repeat(" ", baseIndent) + lines[i] + } + msg = strings.Join(lines, "\n") + + timestamp := timestampStyle.Render(time.Now().Format("15:04:05")) + message := logStyle.Render(msg) + + return fmt.Sprintf("%s %s %s\n", timestamp, levelStyled, message) +} diff --git a/ui/cli/progress.go b/ui/cli/progress.go new file mode 100644 index 0000000..8311618 --- /dev/null +++ b/ui/cli/progress.go @@ -0,0 +1,40 @@ +package cli + +import ( + "github.com/apiqube/cli/ui" + "github.com/pterm/pterm" +) + +func (u *UI) Progress() ui.ProgressReporter { + return &progressReporter{ui: u} +} + +type progressReporter struct { + ui *UI + bar *pterm.ProgressbarPrinter +} + +func (pr *progressReporter) Start(total int, title string) { + p := pterm.DefaultProgressbar. + WithTotal(total). + WithTitle(title). + WithMaxWidth(75). + WithRemoveWhenDone(true). + WithBarCharacter("█"). + WithBarFiller("░"). + WithBarStyle(pterm.NewStyle(pterm.FgLightMagenta)). + WithTitleStyle(pterm.NewStyle(pterm.FgWhite, pterm.Bold)) + + pr.bar, _ = p.Start() +} + +func (pr *progressReporter) Increment(msg string) { + if msg != "" { + pr.bar.UpdateTitle(msg) + } + pr.bar.Increment() +} + +func (pr *progressReporter) Stop() { + _, _ = pr.bar.Stop() +} diff --git a/ui/cli/snipper.go b/ui/cli/snipper.go new file mode 100644 index 0000000..3c6dcdf --- /dev/null +++ b/ui/cli/snipper.go @@ -0,0 +1,30 @@ +package cli + +import ( + "github.com/apiqube/cli/ui" + "github.com/pterm/pterm" +) + +func (u *UI) Snippet() ui.SnippetReporter { + return &snippetReporter{ui: u} +} + +var _ ui.SnippetReporter = (*snippetReporter)(nil) + +type snippetReporter struct { + ui *UI +} + +func (s *snippetReporter) View(title string, data []byte) { + pterm.Println() + + box := pterm.DefaultBox. + WithTitle(pterm.Red(title)). + WithTopPadding(1). + WithLeftPadding(2). + WithBoxStyle(pterm.NewStyle(pterm.FgGray)). + WithTextStyle(pterm.NewStyle(pterm.FgWhite)) + + box.Println(string(data)) + pterm.Println() +} diff --git a/ui/cli/spinner.go b/ui/cli/spinner.go new file mode 100644 index 0000000..7a5c39e --- /dev/null +++ b/ui/cli/spinner.go @@ -0,0 +1,34 @@ +package cli + +import ( + "github.com/apiqube/cli/ui" + "github.com/pterm/pterm" +) + +var spinners = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + +func (u *UI) Spinner() ui.SpinnerReporter { + return &spinnerReporter{ui: u} +} + +var _ ui.SpinnerReporter = (*spinnerReporter)(nil) + +type spinnerReporter struct { + ui *UI + spinner *pterm.SpinnerPrinter +} + +func (s *spinnerReporter) Start(text string) { + spinner := pterm.DefaultSpinner. + WithText(text). + WithShowTimer(true). + WithSequence(spinners...). + WithRemoveWhenDone(true). + WithStyle(pterm.NewStyle(pterm.FgMagenta, pterm.Bold)) + + s.spinner, _ = spinner.Start(text) +} + +func (s *spinnerReporter) Stop(_ string) { + _ = s.spinner.Stop() +} diff --git a/ui/cli/styles.go b/ui/cli/styles.go new file mode 100644 index 0000000..3470dbd --- /dev/null +++ b/ui/cli/styles.go @@ -0,0 +1,31 @@ +package cli + +import "github.com/charmbracelet/lipgloss" + +var ( + timestampStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#949494")) + + logStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#e4e4e4")) + + debugStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#005fff")) + + infoStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#00afff")) + + warningStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#ff8700")) + + errorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#d70000")). + Bold(true) + + fatalStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FF0000")). + Bold(true) + + successStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#5fd700")) +) diff --git a/ui/cli/table.go b/ui/cli/table.go new file mode 100644 index 0000000..341d0c1 --- /dev/null +++ b/ui/cli/table.go @@ -0,0 +1,29 @@ +package cli + +import ( + "github.com/pterm/pterm" +) + +func (u *UI) Table(headers []string, rows [][]string) { + tableData := pterm.TableData{headers} + for _, row := range rows { + tableData = append(tableData, row) + } + + pterm.Println() + + table := pterm.DefaultTable. + WithHasHeader(true). + WithBoxed(false). + WithHeaderStyle(pterm.NewStyle(pterm.FgCyan, pterm.Bold, pterm.BgDefault)). + WithSeparator(" | "). + WithSeparatorStyle(pterm.NewStyle(pterm.FgDarkGray)). + WithData(tableData). + WithLeftAlignment(true). + WithRowSeparator(""). + WithLeftAlignment(true) + + if err := table.Render(); err != nil { + pterm.Error.WithShowLineNumber(false).Printfln("Rendering of the table failed: %v", err) + } +} diff --git a/ui/cli/ui.go b/ui/cli/ui.go new file mode 100644 index 0000000..f9c506e --- /dev/null +++ b/ui/cli/ui.go @@ -0,0 +1,21 @@ +package cli + +import ( + "github.com/apiqube/cli/ui" + "github.com/pterm/pterm" +) + +var _ ui.UI = (*UI)(nil) + +type UI struct { + logger *pterm.Logger +} + +func NewUI() *UI { + return &UI{ + logger: pterm.DefaultLogger. + WithLevel(pterm.LogLevelTrace). + WithTime(true). + WithTimeFormat("2006-01-02 15:04:05"), + } +} diff --git a/ui/console.go b/ui/console.go deleted file mode 100644 index 507d39c..0000000 --- a/ui/console.go +++ /dev/null @@ -1,53 +0,0 @@ -package ui - -func Print(a ...interface{}) { - printStyled(TypeLog, a...) -} - -func Printf(format string, a ...interface{}) { - printStyledf(TypeLog, format, a...) -} - -func Println(a ...interface{}) { - printStyledln(TypeLog, a...) -} - -func Success(a ...interface{}) { - printStyled(TypeSuccess, a...) -} - -func Successf(format string, a ...interface{}) { - printStyledf(TypeSuccess, format, a...) -} - -func Error(a ...interface{}) { - printStyled(TypeError, a...) -} - -func Errorf(format string, a ...interface{}) { - printStyledf(TypeError, format, a...) -} - -func Warning(a ...interface{}) { - printStyled(TypeWarning, a...) -} - -func Warningf(format string, a ...interface{}) { - printStyledf(TypeWarning, format, a...) -} - -func Info(a ...interface{}) { - printStyled(TypeInfo, a...) -} - -func Infof(format string, a ...interface{}) { - printStyledf(TypeInfo, format, a...) -} - -func Debug(a ...interface{}) { - printStyled(TypeDebug, a...) -} - -func Debugf(format string, a ...interface{}) { - printStyledf(TypeDebug, format, a...) -} diff --git a/ui/elements.go b/ui/elements.go deleted file mode 100644 index ac409b3..0000000 --- a/ui/elements.go +++ /dev/null @@ -1,372 +0,0 @@ -package ui - -import ( - "fmt" - "strings" - "time" - - "github.com/charmbracelet/lipgloss" -) - -func Progress(percent float64, text ...string) { - if !IsEnabled() { - return - } - - instance.queueUpdate(func(m *uiModel) { - textStr := "" - if len(text) > 0 { - textStr = strings.Join(text, " ") - } - - if percent >= 100 { - m.removeLastProgress() - return - } - - m.updateOrAddProgress(percent, textStr) - }) -} - -func Loader(show bool, text ...string) { - if !IsEnabled() { - return - } - - instance.queueUpdate(func(m *uiModel) { - textStr := "" - if len(text) > 0 { - textStr = strings.Join(text, " ") - } - - if !show { - m.removeLastLoader() - return - } - - m.updateOrAddLoader(textStr) - }) -} - -func Snippet(code string) { - if IsEnabled() { - instance.queueUpdate(func(m *uiModel) { - m.content = append(m.content, message{ - text: code, - style: snippetStyle, - }) - }) - } -} - -func PackageManager(action, pkg, status string) { - if !IsEnabled() { - return - } - - instance.queueUpdate(func(m *uiModel) { - m.elements = append(m.elements, Element{ - elementType: TypePackage, - action: action, - packageName: pkg, - status: status, - }) - }) -} - -func RealtimeMsg(content string) { - if !IsEnabled() { - return - } - - instance.queueUpdate(func(m *uiModel) { - m.elements = append(m.elements, Element{ - elementType: TypeRealtime, - content: content, - }) - }) -} - -func Spinner(show bool, text ...string) { - if !IsEnabled() { - return - } - - instance.queueUpdate(func(m *uiModel) { - textStr := "" - if len(text) > 0 { - textStr = strings.Join(text, " ") - } - - if !show { - m.removeLastSpinner() - return - } - - m.updateOrAddSpinner(textStr) - }) -} - -func Stopwatch(start bool, name ...string) { - if !IsEnabled() { - return - } - - instance.queueUpdate(func(m *uiModel) { - nameStr := "" - if len(name) > 0 { - nameStr = strings.Join(name, " ") - } - - if !start { - m.removeLastStopwatch() - return - } - - m.updateOrAddStopwatch(nameStr) - }) -} - -func Table(headers []string, data [][]string) { - if !IsEnabled() { - return - } - - instance.queueUpdate(func(m *uiModel) { - m.elements = append(m.elements, Element{ - elementType: TypeTable, - tableHeaders: headers, - tableData: data, - }) - }) -} - -func (m *uiModel) removeLastProgress() { - for i := len(m.elements) - 1; i >= 0; i-- { - if m.elements[i].elementType == TypeProgress { - m.elements = append(m.elements[:i], m.elements[i+1:]...) - return - } - } -} - -func (m *uiModel) removeLastLoader() { - for i := len(m.elements) - 1; i >= 0; i-- { - if m.elements[i].elementType == TypeLoader { - m.elements = append(m.elements[:i], m.elements[i+1:]...) - return - } - } -} - -func (m *uiModel) updateOrAddProgress(percent float64, text string) { - for i := len(m.elements) - 1; i >= 0; i-- { - if m.elements[i].elementType == TypeProgress { - m.elements[i].progress = percent - m.elements[i].progressText = text - return - } - } - - m.elements = append(m.elements, Element{ - elementType: TypeProgress, - progress: percent, - progressText: text, - }) -} - -func (m *uiModel) updateOrAddLoader(text string) { - for i := len(m.elements) - 1; i >= 0; i-- { - if m.elements[i].elementType == TypeLoader { - m.elements[i].showLoader = true - m.elements[i].loaderText = text - return - } - } - - m.elements = append(m.elements, Element{ - elementType: TypeLoader, - showLoader: true, - loaderText: text, - }) -} - -func renderPackage(header, body, status string) string { - actionStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true) - pkgStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("15")) - statusStyle := lipgloss.NewStyle() - - switch status { - case "done": - statusStyle = statusStyle.Foreground(lipgloss.Color("10")) - case "error": - statusStyle = statusStyle.Foreground(lipgloss.Color("9")).Bold(true) - case "working": - statusStyle = statusStyle.Foreground(lipgloss.Color("214")) - default: - statusStyle = statusStyle.Foreground(lipgloss.Color("240")) - } - - return fmt.Sprintf("%s %s %s", - actionStyle.Render(header), - pkgStyle.Render(body), - statusStyle.Render(status), - ) -} - -func renderRealtime(content string) string { - return lipgloss.NewStyle(). - Foreground(lipgloss.Color("45")). - Render("↳ " + content) -} - -func renderSpinner(index int, text string) string { - if text == "" { - text = "Processing" - } - return spinnerStyle. - Render(fmt.Sprintf("%s %s", spinners[index], text)) -} - -func renderStopwatch(startTime time.Time, name string) string { - duration := time.Since(startTime).Round(time.Second) - timeStr := fmt.Sprintf("%02d:%02d:%02d", - int(duration.Hours()), - int(duration.Minutes())%60, - int(duration.Seconds())%60) - - text := timeStr - if name != "" { - text = name + ": " + timeStr - } - - return lipgloss.NewStyle(). - Foreground(lipgloss.Color("51")). - Render("⏱ " + text) -} - -func renderTable(headers []string, data [][]string) string { - if len(headers) == 0 || len(data) == 0 { - return "" - } - - colWidths := make([]int, len(headers)) - for i, h := range headers { - colWidths[i] = len(h) - } - - for _, row := range data { - for i, cell := range row { - if len(cell) > colWidths[i] { - colWidths[i] = len(cell) - } - } - } - - var sb strings.Builder - headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("39")) - cellStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("15")) - - for i, h := range headers { - sb.WriteString(headerStyle.Render(fmt.Sprintf("%-*s", colWidths[i]+2, h))) - } - sb.WriteString("\n") - - for _, w := range colWidths { - sb.WriteString(strings.Repeat("─", w+2)) - } - sb.WriteString("\n") - - for _, row := range data { - for i, cell := range row { - sb.WriteString(cellStyle.Render(fmt.Sprintf("%-*s", colWidths[i]+2, cell))) - } - sb.WriteString("\n") - } - - return sb.String() -} - -func renderProgressBar(percent float64, text string) string { - const width = 30 - filled := int(percent / 100 * width) - unfilled := width - filled - - percentStr := fmt.Sprintf("%3.0f%%", percent) - bar := strings.Repeat("█", filled) + strings.Repeat("░", unfilled) - - if text == "" { - text = fmt.Sprintf("%.1f%%", percent) - } - - percentPart := progressTextStyle.Render(" " + percentStr + " ") - barPart := progressBarStyle.Render(bar) - textPart := progressTextStyle.Render(" " + text) - - return textPart + barPart + percentPart -} - -func renderLoader(text string) string { - if text == "" { - text = "Processing..." - } - return loaderStyle.Render("↻ " + text) -} - -func renderSnippet(code string) string { - return snippetStyle.Render(code) -} - -func (m *uiModel) removeLastSpinner() { - for i := len(m.elements) - 1; i >= 0; i-- { - if m.elements[i].elementType == TypeSpinner { - m.elements = append(m.elements[:i], m.elements[i+1:]...) - return - } - } -} - -func (m *uiModel) updateOrAddSpinner(text string) { - for i := len(m.elements) - 1; i >= 0; i-- { - if m.elements[i].elementType == TypeSpinner { - m.elements[i].showSpinner = true - m.elements[i].spinnerText = text - return - } - } - - m.elements = append(m.elements, Element{ - elementType: TypeSpinner, - showSpinner: true, - spinnerText: text, - }) -} - -func (m *uiModel) removeLastStopwatch() { - for i := len(m.elements) - 1; i >= 0; i-- { - if m.elements[i].elementType == TypeStopwatch { - m.elements = append(m.elements[:i], m.elements[i+1:]...) - return - } - } -} - -func (m *uiModel) updateOrAddStopwatch(name string) { - now := time.Now() - for i := len(m.elements) - 1; i >= 0; i-- { - if m.elements[i].elementType == TypeStopwatch { - if name != "" && m.elements[i].content != name { - continue - } - m.elements[i].startTime = now - m.elements[i].content = name - return - } - } - - m.elements = append(m.elements, Element{ - elementType: TypeStopwatch, - startTime: now, - content: name, - }) -} diff --git a/ui/interface.go b/ui/interface.go new file mode 100644 index 0000000..f69e613 --- /dev/null +++ b/ui/interface.go @@ -0,0 +1,44 @@ +package ui + +type LogLevel uint8 + +const ( + TypeLog LogLevel = iota + 1 + TypeDebug + TypeInfo + TypeWarning + TypeError + TypeFatal + TypeSuccess +) + +type UI interface { + Log(level LogLevel, msg string) + Logf(level LogLevel, format string, args ...any) + Progress() ProgressReporter + Snippet() SnippetReporter + Spinner() SpinnerReporter + Table(headers []string, rows [][]string) + Error(err error) + Done(msg string) +} + +type ProgressReporter interface { + Start(total int, title string) + Increment(msg string) + Stop() +} + +type LoaderReporter interface { + Start(text string) + Stop(text string) +} + +type SnippetReporter interface { + View(title string, data []byte) +} + +type SpinnerReporter interface { + Start(text string) + Stop(text string) +} diff --git a/ui/model.go b/ui/model.go deleted file mode 100644 index 8864c8d..0000000 --- a/ui/model.go +++ /dev/null @@ -1,98 +0,0 @@ -package ui - -import ( - "strings" - "sync" - "time" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -type uiModel struct { - elements []Element - content []message - mu sync.Mutex - spinnerIndex int -} - -type message struct { - text string - style lipgloss.Style - timestamp time.Time -} - -type updateFunc func(*uiModel) - -type forceRefresh struct{} - -func (m *uiModel) Init() tea.Cmd { - return nil -} - -func (m *uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg.(type) { - case tea.KeyMsg: - return m, tea.Quit - case forceRefresh: - return m, nil - } - return m, nil -} - -func (m *uiModel) View() string { - m.mu.Lock() - defer m.mu.Unlock() - - var sb strings.Builder - - m.spinnerIndex = (m.spinnerIndex + 1) % len(spinners) - - for _, msg := range m.content { - sb.WriteString(timestampStyle.Render(msg.timestamp.Format("15:04:05")) + " " + msg.style.Render(msg.text)) - sb.WriteString("\n") - } - - for _, elem := range m.elements { - switch elem.elementType { - case TypeProgress: - sb.WriteString(renderProgressBar(elem.progress, elem.progressText)) - sb.WriteString("\n\n") - - case TypeLoader: - if elem.showLoader { - sb.WriteString(renderLoader(elem.loaderText)) - sb.WriteString("\n\n") - } - - case TypePackage: - sb.WriteString(renderPackage(elem.action, elem.packageName, elem.status)) - sb.WriteString("\n") - - case TypeRealtime: - sb.WriteString(renderRealtime(elem.content)) - sb.WriteString("\n") - - case TypeSpinner: - if elem.showSpinner { - sb.WriteString(renderSpinner(m.spinnerIndex, elem.spinnerText)) - sb.WriteString("\n") - } - - case TypeStopwatch: - sb.WriteString(renderStopwatch(elem.startTime, elem.content)) - sb.WriteString("\n") - - case TypeTable: - sb.WriteString(renderTable(elem.tableHeaders, elem.tableData)) - sb.WriteString("\n") - - case TypeSnippet: - sb.WriteString(renderSnippet(elem.content)) - sb.WriteString("\n\n") - default: - } - } - - return sb.String() -} diff --git a/ui/styles.go b/ui/styles.go deleted file mode 100644 index 847a78b..0000000 --- a/ui/styles.go +++ /dev/null @@ -1,106 +0,0 @@ -package ui - -import ( - "fmt" - "time" - - "github.com/charmbracelet/lipgloss" -) - -var ( - timestampStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#949494")) - - logStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#e4e4e4")) - - successStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#5fd700")) - - errorStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FF0000")) - - warningStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#ff8700")) - - infoStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#00afff")) - - debugStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#005fff")) - - snippetStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("230")). - Background(lipgloss.Color("236")). - Padding(0, 1). - Border(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("59")) - - progressBarStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("57")). - Background(lipgloss.Color("236")) - - progressTextStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("255")) - - loaderStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("212")) - - spinnerStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#ff0087")) -) - -func getStyle(t MessageType) lipgloss.Style { - switch t { - case TypeSuccess: - return successStyle - case TypeError: - return errorStyle - case TypeWarning: - return warningStyle - case TypeInfo: - return infoStyle - case TypeDebug: - return debugStyle - case TypeSnippet: - return snippetStyle - default: - return logStyle - } -} - -func printStyled(t MessageType, a ...interface{}) { - if IsEnabled() { - instance.queueUpdate(func(m *uiModel) { - m.content = append(m.content, message{ - text: fmt.Sprint(a...), - style: getStyle(t), - timestamp: time.Now(), - }) - }) - } -} - -func printStyledf(t MessageType, format string, a ...interface{}) { - if IsEnabled() { - instance.queueUpdate(func(m *uiModel) { - m.content = append(m.content, message{ - text: fmt.Sprintf(format, a...), - style: getStyle(t), - timestamp: time.Now(), - }) - }) - } -} - -func printStyledln(t MessageType, a ...interface{}) { - if IsEnabled() { - instance.queueUpdate(func(m *uiModel) { - m.content = append(m.content, message{ - text: fmt.Sprintln(a...), - style: getStyle(t), - timestamp: time.Now(), - }) - }) - } -} diff --git a/ui/ui.go b/ui/ui.go deleted file mode 100644 index b273c72..0000000 --- a/ui/ui.go +++ /dev/null @@ -1,156 +0,0 @@ -package ui - -import ( - "fmt" - "os" - "sync" - "time" - - tea "github.com/charmbracelet/bubbletea" -) - -type MessageType int - -const ( - TypeLog MessageType = iota - TypeProgress - TypeLoader - TypeSuccess - TypeError - TypeWarning - TypeInfo - TypeDebug - TypeSnippet - TypePackage - TypeRealtime - TypeSpinner - TypeStopwatch - TypeTable -) - -type Element struct { - elementType MessageType - content string - progress float64 - progressText string - showLoader bool - loaderText string - showSpinner bool - spinnerText string - startTime time.Time - tableData [][]string - tableHeaders []string - status string - packageName string - action string -} - -var ( - instance *UI - once sync.Once - spinners = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} -) - -type UI struct { - program *tea.Program - model *uiModel - enabled bool - updates chan updateFunc - closeCh chan struct{} - wg sync.WaitGroup - initialized bool - ready chan struct{} -} - -func Init() { - once.Do(func() { - model := &uiModel{} - ui := &UI{ - model: model, - enabled: true, - updates: make(chan updateFunc, 100), - closeCh: make(chan struct{}), - ready: make(chan struct{}), - } - - go func() { - ui.program = tea.NewProgram( - model, - tea.WithInput(nil), - tea.WithOutput(os.Stderr), - ) - close(ui.ready) - if _, err := ui.program.Run(); err != nil { - fmt.Println("UI error:", err) - } - }() - - ui.wg.Add(1) - go ui.processUpdates() - - instance = ui - instance.initialized = true - }) -} - -func Stop() { - if instance != nil && instance.initialized { - close(instance.closeCh) - instance.wg.Wait() - instance.enabled = false - instance.initialized = false - instance = nil - } -} - -func StopWithTimeout(timeout time.Duration) { - if instance != nil && instance.initialized { - time.AfterFunc(timeout, func() { - Stop() - }) - } -} - -func IsEnabled() bool { - return instance != nil && instance.enabled -} - -func (ui *UI) queueUpdate(fn updateFunc) { - select { - case ui.updates <- fn: - default: - } -} - -func (ui *UI) processUpdates() { - defer ui.wg.Done() - - <-ui.ready - - ticker := time.NewTicker(100 * time.Millisecond) - defer ticker.Stop() - - for { - select { - case update := <-ui.updates: - ui.model.mu.Lock() - update(ui.model) - ui.model.mu.Unlock() - ui.requestRender() - - case <-ticker.C: - ui.requestRender() - - case <-ui.closeCh: - return - } - } -} - -func (ui *UI) requestRender() { - if ui.program != nil { - go func() { - ui.program.Send(forceRefresh{}) - }() - } -}