diff --git a/cmd/root/root.go b/cmd/root/root.go index 4ad6e77..98feaec 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -9,6 +9,7 @@ import ( "github.com/open-sauced/pizza-cli/cmd/auth" "github.com/open-sauced/pizza-cli/cmd/bake" repoquery "github.com/open-sauced/pizza-cli/cmd/repo-query" + "github.com/open-sauced/pizza-cli/cmd/show" "github.com/open-sauced/pizza-cli/pkg/api" ) @@ -27,6 +28,7 @@ func NewRootCommand() (*cobra.Command, error) { cmd.AddCommand(bake.NewBakeCommand()) cmd.AddCommand(repoquery.NewRepoQueryCommand()) cmd.AddCommand(auth.NewLoginCommand()) + cmd.AddCommand(show.NewShowCommand()) return cmd, nil } diff --git a/cmd/show/show.go b/cmd/show/show.go new file mode 100644 index 0000000..e90c3b0 --- /dev/null +++ b/cmd/show/show.go @@ -0,0 +1,278 @@ +// Package show contains the bootstrapping and tooling for the pizza show +// cobra command +package show + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" +) + +// Options are the options for the pizza show command including user +// defined configurations +type Options struct { + // RepoName is the git repo name to get statistics + RepoName string + + // Page is the page to be requested + Page int + + // Limit is the number of records to be retrieved + Limit int + + // Range is the number of days to take into account when retrieving statistics + Range int +} + +const showLongDesc string = `WARNING: Proof of concept feature. + +The show command accepts the name of a git repository in the format 'owner/name' and uses OpenSauced api +to retrieve metrics of the repository to be displayed as a TUI.` + +// NewBakeCommand returns a new cobra command for 'pizza bake' +func NewShowCommand() *cobra.Command { + opts := &Options{} + + cmd := &cobra.Command{ + Use: "show repository-name [flags]", + Short: "Get visual metrics of a repository", + Long: showLongDesc, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("must specify the URL of a git repository to analyze") + } + opts.RepoName = args[0] + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return run(opts) + }, + } + + cmd.Flags().IntVarP(&opts.Range, "range", "r", 30, "The last N number of days to consider") + cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 10, "The number of records to retrieve") + cmd.Flags().IntVarP(&opts.Page, "page", "p", 1, "The page number to retrieve") + + return cmd +} + +type Author struct { + Name string `json:"author_login"` +} + +type Metadata struct { + Page int `json:"page"` + Limit int `json:"limit"` + ItemCount int `json:"itemCount"` + PageCount int `json:"pageCount"` + HasPreviousPage bool `json:"hasPreviousPage"` + HasNextPage bool `json:"hasNextPage"` +} + +type showRespose struct { + Contributors []Author `json:"data"` + Metadata Metadata `json:"meta"` +} + +type showResponseResult struct { + ShowRespose showRespose + Error error +} + +type reposResponse struct { + ID int `json:"id"` + Size int `json:"size"` + Issues int `json:"issues"` + Stars int `json:"stars"` + Forks int `json:"forks"` + Watchers int `json:"watchers"` + Subscribers int `json:"subscribers"` + Network int `json:"network"` + IsFork bool `json:"is_fork"` + IsPrivate bool `json:"is_private"` + IsTemplate bool `json:"is_template"` + IsArchived bool `json:"is_archived"` + IsDisabled bool `json:"is_disabled"` + HasIssues bool `json:"has_issues"` + HasProjects bool `json:"has_projects"` + HasDownloads bool `json:"has_downloads"` + HasWiki bool `json:"has_wiki"` + HasPages bool `json:"has_pages"` + HasDisscussions bool `json:"has_discussions"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PushedAt time.Time `json:"pushed_at"` + DefaultBranch string `json:"default_branch"` + NodeID string `json:"node_id"` + GitURL string `json:"git_url"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` + SVNURL string `json:"svn_url"` + MirrorURL string `json:"mirror_url"` + Name string `json:"name"` + FullName string `json:"full_name"` + Description string `json:"description"` + Language string `json:"language"` + License string `json:"license"` + URL string `json:"url"` + Homepage string `json:"homepage"` + Topics []string `json:"topics"` + ContributionCount int `json:"contributionsCount"` + VotesCount int `json:"votesCount"` + SubmissionCount int `json:"submissionsCount"` + StargazersCount int `json:"stargazersCount"` + StarsCount int `json:"starsCount"` +} + +func run(opts *Options) error { + + err := validateShowQuery(opts) + if err != nil { + return err + } + + var client = &http.Client{Timeout: 10 * time.Second} + + // retrieve repository info + resp, err := client.Get(fmt.Sprintf("https://api.opensauced.pizza/v1/repos/%s", opts.RepoName)) + if err != nil { + return err + } + defer resp.Body.Close() + + var repoInfo reposResponse + err = json.NewDecoder(resp.Body).Decode(&repoInfo) + if err != nil { + return err + } + + if repoInfo.ID == 0 { + return errors.New("could not find repository") + } + + newContributors, err := FetchNewContributors(opts, repoInfo.ID) + if err != nil { + return err + } + + churnContributors, err := FetchChurnedContributors(opts, repoInfo.ID) + if err != nil { + return err + } + + // create model for the TUI + model := Model{ + RepositoryInfo: repoInfo, + newContributorsTable: setupTable(newContributors.Contributors), + churnContributorsTable: setupTable(churnContributors.Contributors), + curView: 0, + } + + // Load the pizza TUI + pizzaTUI(model) + + return nil +} + +func validateShowQuery(opts *Options) error { + if opts.RepoName == "" { + return errors.New("no repository was provided") + } + + if len(strings.Split(opts.RepoName, "/")) != 2 { + return errors.New("wrong repository name format. Must be in the form owner/name, i.e(open-sauced/pizza)") + } + if opts.Limit < 1 { + return errors.New("--limit flag must be a positive integer value") + } + if opts.Range < 1 { + return errors.New("--range flag must be a positive integer value") + } + + if opts.Page < 1 { + return errors.New("--page flag must be a positive integer value") + } + + return nil +} + +// FetchNewContributors: Returns all the new contributors +func FetchNewContributors(opts *Options, repoID int) (showRespose, error) { + var client = &http.Client{Timeout: 10 * time.Second} + var newContributors showRespose + + resp, err := client.Get(fmt.Sprintf("https://api.opensauced.pizza/v1/contributors/insights/new?page=%d&limit=%d&range=%d&repoIds=%d", + opts.Page, opts.Limit, opts.Range, repoID)) + if err != nil { + return newContributors, err + } + defer resp.Body.Close() + + err = json.NewDecoder(resp.Body).Decode(&newContributors) + if err != nil { + return newContributors, err + } + return newContributors, nil +} + +// FetchChurnedContributors: Returns all churned contributors +func FetchChurnedContributors(opts *Options, repoID int) (showRespose, error) { + // retrieve churn contributors info + var client = &http.Client{Timeout: 10 * time.Second} + var churnContributors showRespose + + resp, err := client.Get(fmt.Sprintf("https://api.opensauced.pizza/v1/contributors/insights/churn?page=%d&limit=%d&range=%d&repoIds=%d", + opts.Page, opts.Limit, opts.Range, repoID)) + if err != nil { + return churnContributors, err + } + defer resp.Body.Close() + + err = json.NewDecoder(resp.Body).Decode(&churnContributors) + if err != nil { + return churnContributors, err + } + + return churnContributors, nil +} + +func setupTable(contributors []Author) table.Model { + columns := []table.Column{ + {Title: "ID", Width: 5}, + {Title: "Name", Width: 20}, + } + + rows := []table.Row{} + + for i, contributor := range contributors { + rows = append(rows, table.Row{strconv.Itoa(i), contributor.Name}) + } + + contributorTable := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(15), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderBottom(true). + Bold(true) + + s.Selected = s.Selected. + Foreground(lipgloss.Color("#FFFFFF")). + Background(lipgloss.Color("#FF4500")) + + contributorTable.SetStyles(s) + return contributorTable +} diff --git a/cmd/show/styles.go b/cmd/show/styles.go new file mode 100644 index 0000000..41e1ae3 --- /dev/null +++ b/cmd/show/styles.go @@ -0,0 +1,25 @@ +package show + +import "github.com/charmbracelet/lipgloss" + +// Container: container styling (width: 80, py: 0, px: 5) +var container = lipgloss.NewStyle().Width(80).Padding(0, 5) + +// WidgetContainer: container for tables, and graphs (py:2, px:2) +var widgetContainer = lipgloss.NewStyle().Padding(2, 2) + +// TableTitle: The style for table titles (width:25, align-horizontal:center, bold:true) +var tableTitle = lipgloss.NewStyle().Width(25).AlignHorizontal(lipgloss.Center).Bold(true) + +// Color: the color palette (Light: #000000, Dark: #FF4500) +var color = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FF4500"} + +// ActiveStyle: table when selected (border:normal, border-foreground:#FF4500) +var activeStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#FF4500")) + +// InactiveStyle: table when unselected (border: normal, border-foreground:#FFFFFF) +var inactiveStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#FFFFFF")) diff --git a/cmd/show/tui.go b/cmd/show/tui.go new file mode 100644 index 0000000..5c275c1 --- /dev/null +++ b/cmd/show/tui.go @@ -0,0 +1,76 @@ +package show + +import ( + "fmt" + "os" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// ██████╗ ██╗███████╗███████╗ █████╗ +// ██╔══██╗██║╚══███╔╝╚══███╔╝██╔══██╗ +// ██████╔╝██║ ███╔╝ ███╔╝ ███████║ +// ██╔═══╝ ██║ ███╔╝ ███╔╝ ██╔══██║ +// ██║ ██║███████╗███████╗██║ ██║ +// ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ + +const ( + newContributorsView = iota + churnContributorsView +) + +type Model struct { + newContributorsTable table.Model + churnContributorsTable table.Model + RepositoryInfo reposResponse + curView int +} + +// View: the view of the TUI +func (m Model) View() string { + titleView, repoInfoView, metricsView := m.drawTitle(), m.drawRepositoryInfo(), m.drawMetrics() + mainView := lipgloss.JoinVertical(lipgloss.Center, titleView, repoInfoView, metricsView) + + return mainView +} + +// Init: initial IO before program start +func (m Model) Init() tea.Cmd { return nil } + +// Update: Handle IO and Commands +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "right", "l": + m.curView = (m.curView + 1) % 2 + case "left", "h": + if m.curView-1 <= 0 { + m.curView = 0 + } else { + m.curView-- + } + case "q", "ctrl+c": + return m, tea.Quit + } + + switch m.curView { + case newContributorsView: + m.newContributorsTable, cmd = m.newContributorsTable.Update(msg) + case churnContributorsView: + m.churnContributorsTable, cmd = m.churnContributorsTable.Update(msg) + } + } + + return m, cmd +} + +func pizzaTUI(model Model) { + if _, err := tea.NewProgram(model, tea.WithAltScreen()).Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/cmd/show/view.go b/cmd/show/view.go new file mode 100644 index 0000000..a7ddf38 --- /dev/null +++ b/cmd/show/view.go @@ -0,0 +1,63 @@ +package show + +import ( + "fmt" + + "github.com/charmbracelet/lipgloss" +) + +// drawTitle: view of PIZZA +func (m Model) drawTitle() string { + titleRunes1 := []rune{'█', '█', '█', '█', '█', '█', '╗', ' ', '█', '█', '╗', '█', '█', '█', '█', '█', '█', '█', '╗', '█', '█', '█', '█', '█', '█', '█', '╗', ' ', '█', '█', '█', '█', '█', '╗', ' '} + titleRunes2 := []rune{'█', '█', '╔', '═', '═', '█', '█', '╗', '█', '█', '║', '╚', '═', '═', '█', '█', '█', '╔', '╝', '╚', '═', '═', '█', '█', '█', '╔', '╝', '█', '█', '╔', '═', '═', '█', '█', '╗'} + titleRunes3 := []rune{'█', '█', '█', '█', '█', '█', '╔', '╝', '█', '█', '║', ' ', ' ', '█', '█', '█', '╔', '╝', ' ', ' ', ' ', '█', '█', '█', '╔', '╝', ' ', '█', '█', '█', '█', '█', '█', '█', '║'} + titleRunes4 := []rune{'█', '█', '╔', '═', '═', '═', '╝', ' ', '█', '█', '║', ' ', '█', '█', '█', '╔', '╝', ' ', ' ', ' ', '█', '█', '█', '╔', '╝', ' ', ' ', '█', '█', '╔', '═', '═', '█', '█', '║'} + titleRunes5 := []rune{'█', '█', '║', ' ', ' ', ' ', ' ', ' ', '█', '█', '║', '█', '█', '█', '█', '█', '█', '█', '╗', '█', '█', '█', '█', '█', '█', '█', '╗', '█', '█', '║', ' ', ' ', '█', '█', '║'} + titleRunes6 := []rune{'╚', '═', '╝', ' ', ' ', ' ', ' ', ' ', '╚', '═', '╝', '╚', '═', '═', '═', '═', '═', '═', '╝', '╚', '═', '═', '═', '═', '═', '═', '╝', '╚', '═', '╝', ' ', ' ', '╚', '═', '╝'} + + title1 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes1)) + title2 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes2)) + title3 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes3)) + title4 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes4)) + title5 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes5)) + title6 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes6)) + title := lipgloss.JoinVertical(lipgloss.Center, title1, title2, title3, title4, title5, title6) + titleView := lipgloss.NewStyle().Width(m.newContributorsTable.Width()).Margin(2, 60).Foreground(color).Render(title) + + return titleView +} + +// drawRepositoryTitle: view of the repository info (name, stars) +func (m Model) drawRepositoryInfo() string { + repoName := lipgloss.NewStyle().Bold(true).Render(fmt.Sprintf("Repository: %s", m.RepositoryInfo.FullName)) + repoStars := fmt.Sprintf("⭐️ stars: %d", m.RepositoryInfo.Stars) + repoIssues := fmt.Sprintf("📄 issues: %d", m.RepositoryInfo.Issues) + + issuesAndStars := lipgloss.JoinHorizontal(lipgloss.Left, widgetContainer.Render(repoIssues), widgetContainer.Render(repoStars)) + repositoryInfoView := lipgloss.JoinVertical(lipgloss.Center, repoName, issuesAndStars) + + re := lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(color).Render(repositoryInfoView) + + return re +} + +// drawMetrics: view of metrics includes. +// - new contributors table +// - churn contributors table +func (m Model) drawMetrics() string { + var newContributorsDisplay, churnedContributorsDisplay string + + switch m.curView { + case newContributorsView: + newContributorsDisplay = lipgloss.JoinVertical(lipgloss.Center, tableTitle.Render("🍕 New Contributors"), activeStyle.Render(m.newContributorsTable.View())) + churnedContributorsDisplay = lipgloss.JoinVertical(lipgloss.Center, tableTitle.Render("🍁 Churn Contributors"), inactiveStyle.Render(m.churnContributorsTable.View())) + case churnContributorsView: + newContributorsDisplay = lipgloss.JoinVertical(lipgloss.Center, tableTitle.Render("🍕 New Contributors"), inactiveStyle.Render(m.newContributorsTable.View())) + churnedContributorsDisplay = lipgloss.JoinVertical(lipgloss.Center, tableTitle.Render("🍁 Churn Contributors"), activeStyle.Render(m.churnContributorsTable.View())) + } + + contributorsMetrics := lipgloss.JoinHorizontal(lipgloss.Center, widgetContainer.Render(newContributorsDisplay), widgetContainer.Render(churnedContributorsDisplay)) + metricsView := container.Render(contributorsMetrics) + + return metricsView +} diff --git a/go.mod b/go.mod index 1972ac4..ce7e199 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/open-sauced/pizza-cli go 1.20 require ( + github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.7.1 github.com/cli/browser v1.2.0 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 @@ -11,6 +14,19 @@ require ( ) require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.9.0 // indirect + golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index a8adb90..363f78b 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,54 @@ github.com/cli/browser v1.2.0 h1:yvU7e9qf97kZqGFX6n2zJPHsmSObY9ske+iCvKelvXg= github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI= +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/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +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/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=