├── .gitignore ├── main.go ├── renovate.json ├── Taskfile.yaml ├── .github └── workflows │ └── release.yml ├── .vscode └── launch.json ├── README.md ├── pkg ├── search │ └── bleve.go ├── api │ ├── graphql_test.go │ ├── graphql.go │ └── repository.go └── ui │ ├── search.go │ └── fetch.go ├── cmd └── root.go ├── internal └── cache │ └── cache.go ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | /gh-find-starred 2 | /gh-find-starred.exe 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/rokuosan/gh-find-starred/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /Taskfile.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | tasks: 4 | default: 5 | silent: true 6 | cmds: 7 | - task --list-all 8 | 9 | run: 10 | silent: true 11 | cmds: 12 | - go build 13 | - gh find-starred {{.CLI_ARGS}} 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | permissions: 7 | contents: write 8 | id-token: write 9 | attestations: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: cli/gh-extension-precompile@v2 17 | with: 18 | generate_attestations: true 19 | go_version_file: go.mod 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch Package", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "auto", 9 | "console": "integratedTerminal", 10 | "program": "${workspaceFolder}", 11 | "args": [ 12 | "the", 13 | "go", 14 | "programming", 15 | "language" 16 | ], 17 | }, 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gh-find-starred 2 | 3 | A tool to search for repositories you have starred on GitHub. 4 | 5 | You can search for repositories by name and description, and collect information from **README**s. 6 | 7 | ## Prerequisites 8 | 9 | - [gh(GitHub CLI)](https://cli.github.com/) 10 | 11 | ## Installation 12 | 13 | ```bash 14 | $ gh extension install https://github.com/rokuosan/gh-find-starred 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```bash 20 | $ gh find-starred [flags] [words...] 21 | ``` 22 | 23 | ## Examples 24 | 25 | ```bash 26 | $ gh find-starred github 27 | ``` 28 | 29 | ## Flags 30 | 31 | - `-h`, `--help`: Display help. 32 | 33 | ## Planned Flags (Not yet implemented) 34 | 35 | - `-l`, `--limit`: Limit the number of search results displayed. The default is 10. 36 | - `-o`, `--order`: Specify the order of search results. The default is `desc`. 37 | - `-s`, `--sort`: Specify the sort field for search results. The default is `starred_at`. 38 | - `-t`, `--type`: Specify the type of repositories in the search results. The default is `all`. 39 | - `-u`, `--user`: Specify the user to search for. The default is the logged-in user. 40 | - `-v`, `--verbose`: Display detailed information. 41 | -------------------------------------------------------------------------------- /pkg/search/bleve.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/blevesearch/bleve/v2" 8 | "github.com/rokuosan/gh-find-starred/pkg/api" 9 | ) 10 | 11 | type SearchRepositoryResult struct { 12 | Repository api.GitHubRepository 13 | Score float64 14 | } 15 | 16 | func BleveSearch(repositories api.GitHubRepositories, query []string) []SearchRepositoryResult { 17 | mapping := bleve.NewIndexMapping() 18 | index, err := bleve.NewMemOnly(mapping) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | nameToRepo := make(map[string]api.GitHubRepository) 24 | for _, repo := range repositories { 25 | nameToRepo[repo.Name] = repo 26 | index.Index(repo.Name, repo) 27 | } 28 | 29 | var results []SearchRepositoryResult 30 | q := bleve.NewQueryStringQuery(strings.Join(query, " ")) 31 | searchRequest := bleve.NewSearchRequest(q) 32 | searchResult, err := index.Search(searchRequest) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | for _, hit := range searchResult.Hits { 38 | results = append(results, SearchRepositoryResult{ 39 | Repository: nameToRepo[hit.ID], 40 | Score: hit.Score, 41 | }) 42 | } 43 | 44 | // ポイントの高い順にソート 45 | sort.Slice(results, func(i, j int) bool { 46 | return results[i].Score > results[j].Score 47 | }) 48 | 49 | return results 50 | } 51 | -------------------------------------------------------------------------------- /pkg/api/graphql_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func createStarredRepository(name, url, description, readme string) StarredRepository { 10 | return StarredRepository{ 11 | Name: name, 12 | Url: url, 13 | Description: description, 14 | Object: TextObject{ 15 | Blob: struct{ Text string }{ 16 | Text: readme, 17 | }, 18 | }, 19 | } 20 | } 21 | 22 | func createStarredRepositoriesQuery(nodes []StarredRepository, pageInfo PageInfo) *getStarredRepositoriesQuery { 23 | return &getStarredRepositoriesQuery{ 24 | Viewer: struct { 25 | StarredRepositories struct { 26 | Nodes []StarredRepository 27 | PageInfo PageInfo `graphql:"pageInfo"` 28 | } `graphql:"starredRepositories(after: $after, first: 100)"` 29 | }{ 30 | StarredRepositories: struct { 31 | Nodes []StarredRepository 32 | PageInfo PageInfo `graphql:"pageInfo"` 33 | }{ 34 | Nodes: nodes, 35 | PageInfo: pageInfo, 36 | }, 37 | }, 38 | } 39 | } 40 | 41 | func Test_getStarredRepositoriesQuery_Repositories(t *testing.T) { 42 | tests := []struct { 43 | name string 44 | query *getStarredRepositoriesQuery 45 | want GitHubRepositories 46 | }{ 47 | { 48 | name: "test", 49 | query: createStarredRepositoriesQuery( 50 | []StarredRepository{ 51 | createStarredRepository("1", "", "", ""), 52 | createStarredRepository("2", "", "", ""), 53 | }, 54 | PageInfo{}, 55 | ), 56 | want: GitHubRepositories{ 57 | { 58 | Name: "1", 59 | }, 60 | { 61 | Name: "2", 62 | }, 63 | }, 64 | }, 65 | } 66 | 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | assert.Equal(t, tt.query.Repositories(), tt.want) 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/ui/search.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/charmbracelet/bubbles/spinner" 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/rokuosan/gh-find-starred/pkg/api" 11 | "github.com/rokuosan/gh-find-starred/pkg/search" 12 | ) 13 | 14 | type SearchMsg struct { 15 | Result []search.SearchRepositoryResult 16 | } 17 | 18 | type SearchModel struct { 19 | Spinner spinner.Model 20 | Repositories api.GitHubRepositories 21 | Result []search.SearchRepositoryResult 22 | SearchQuery []string 23 | Loading bool 24 | } 25 | 26 | func NewDefaultSearchModel(query []string) SearchModel { 27 | s := spinner.New(spinner.WithSpinner(spinner.Points)) 28 | s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) 29 | 30 | return SearchModel{ 31 | SearchQuery: query, 32 | Spinner: s, 33 | Loading: true, 34 | } 35 | } 36 | 37 | func (m SearchModel) Init() tea.Cmd { 38 | return tea.Batch( 39 | m.Spinner.Tick, 40 | m.Search, 41 | ) 42 | } 43 | 44 | func (m SearchModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 45 | switch msg := msg.(type) { 46 | case spinner.TickMsg: 47 | var cmd tea.Cmd 48 | m.Spinner, cmd = m.Spinner.Update(msg) 49 | return m, cmd 50 | case SearchMsg: 51 | m.Loading = false 52 | m.Result = msg.Result 53 | return m, tea.Quit 54 | } 55 | 56 | return m, nil 57 | } 58 | 59 | func (m SearchModel) View() string { 60 | if m.Loading { 61 | return fmt.Sprintf("%s Searching...", m.Spinner.View()) 62 | } 63 | 64 | var sb strings.Builder 65 | for _, r := range m.Result { 66 | sb.WriteString(fmt.Sprintf("%.1f%% %s %s\n", r.Score*100, r.Repository.Name, r.Repository.Url)) 67 | } 68 | return sb.String() 69 | } 70 | 71 | func (m SearchModel) Search() tea.Msg { 72 | result := search.BleveSearch(m.Repositories, m.SearchQuery) 73 | return SearchMsg{Result: result} 74 | } 75 | -------------------------------------------------------------------------------- /pkg/api/graphql.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/cli/go-gh/v2/pkg/api" 4 | 5 | type RateLimit struct { 6 | Cost int `graphql:"cost" json:"cost"` 7 | Limit int `graphql:"limit" json:"limit"` 8 | Remaining int `graphql:"remaining" json:"remaining"` 9 | ResetAt string `graphql:"resetAt" json:"resetAt"` 10 | } 11 | 12 | type PageInfo struct { 13 | HasNextPage bool `graphql:"hasNextPage" json:"hasNextPage"` 14 | HasPreviousPage bool `graphql:"hasPreviousPage" json:"hasPreviousPage"` 15 | StartCursor string `graphql:"startCursor" json:"startCursor"` 16 | EndCursor string `graphql:"endCursor" json:"endCursor"` 17 | } 18 | 19 | type TextObject struct { 20 | Blob struct { 21 | Text string 22 | } `graphql:"... on Blob"` 23 | } 24 | 25 | type StarredRepository struct { 26 | Name string `json:"name"` 27 | Url string `json:"url"` 28 | Description string `json:"description"` 29 | 30 | Object TextObject `graphql:"object(expression: $expression)"` 31 | } 32 | 33 | type GraphQLQuery interface { 34 | Execute(*api.GraphQLClient, map[string]interface{}) error 35 | } 36 | 37 | type getStarredRepositoriesQuery struct { 38 | Viewer struct { 39 | StarredRepositories struct { 40 | Nodes []StarredRepository 41 | PageInfo PageInfo `graphql:"pageInfo"` 42 | } `graphql:"starredRepositories(after: $after, first: 100)"` 43 | } `graphql:"viewer"` 44 | RateLimit RateLimit `graphql:"rateLimit"` 45 | } 46 | 47 | func (q *getStarredRepositoriesQuery) Repositories() GitHubRepositories { 48 | repos := make([]GitHubRepository, len(q.Viewer.StarredRepositories.Nodes)) 49 | for i, node := range q.Viewer.StarredRepositories.Nodes { 50 | repos[i] = GitHubRepository{ 51 | Name: node.Name, 52 | Url: node.Url, 53 | Description: node.Description, 54 | Readme: node.Object.Blob.Text, 55 | } 56 | } 57 | return repos 58 | } 59 | 60 | func (q *getStarredRepositoriesQuery) PageInfo() PageInfo { 61 | return q.Viewer.StarredRepositories.PageInfo 62 | } 63 | 64 | func (q *getStarredRepositoriesQuery) Execute(client *api.GraphQLClient, variables map[string]interface{}) error { 65 | return client.Query("GetStarredRepositoriesQuery", q, variables) 66 | } 67 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/charmbracelet/bubbles/spinner" 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/rokuosan/gh-find-starred/pkg/api" 11 | "github.com/rokuosan/gh-find-starred/pkg/ui" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var rootCmd = &cobra.Command{ 16 | Use: "gh-find-starred", 17 | Short: "gh-find-starred is a GitHub CLI extension to find your starred repositories.", 18 | Long: ``, 19 | Args: cobra.MinimumNArgs(1), 20 | Run: func(cmd *cobra.Command, args []string) { 21 | p := tea.NewProgram(initialModel(args)) 22 | if _, err := p.Run(); err != nil { 23 | fmt.Println(err) 24 | os.Exit(1) 25 | } 26 | }, 27 | } 28 | 29 | func Execute() { 30 | cobra.CheckErr(rootCmd.Execute()) 31 | } 32 | 33 | func init() {} 34 | 35 | type model struct { 36 | fetch ui.FetchingModel 37 | search ui.SearchModel 38 | repositories api.GitHubRepositories 39 | } 40 | 41 | func initialModel(args []string) model { 42 | // モデルを初期化 43 | return model{ 44 | fetch: ui.NewDefaultFetchingModel(), 45 | search: ui.NewDefaultSearchModel(args), 46 | } 47 | } 48 | 49 | func (m model) Init() tea.Cmd { 50 | return m.fetch.Init() 51 | } 52 | 53 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 54 | switch msg := msg.(type) { 55 | case tea.KeyMsg: 56 | switch msg.String() { 57 | case "q", "esc", "ctrl+c": 58 | return m, tea.Quit 59 | } 60 | case ui.FetchMsg: 61 | fetchModel, fetchCmd := m.fetch.Update(msg) 62 | m.fetch = fetchModel.(ui.FetchingModel) 63 | if m.fetch.Status == ui.FetchStatusCompleted { 64 | m.search.Repositories = m.fetch.Repositories 65 | return m, m.search.Init() 66 | } 67 | return m, fetchCmd 68 | case ui.SearchMsg: 69 | searchModel, searchCmd := m.search.Update(msg) 70 | m.search = searchModel.(ui.SearchModel) 71 | return m, searchCmd 72 | case spinner.TickMsg: 73 | if m.fetch.Status == ui.FetchStatusLoading { 74 | fetchModel, fetchCmd := m.fetch.Update(msg) 75 | m.fetch = fetchModel.(ui.FetchingModel) 76 | return m, fetchCmd 77 | } 78 | if m.search.Loading { 79 | searchModel, searchCmd := m.search.Update(msg) 80 | m.search = searchModel.(ui.SearchModel) 81 | return m, searchCmd 82 | } 83 | } 84 | 85 | return m, nil 86 | } 87 | 88 | func (m model) View() string { 89 | var sb strings.Builder 90 | sb.WriteString(fmt.Sprintf("%s\n", m.fetch.View())) 91 | if m.fetch.Status == ui.FetchStatusCompleted { 92 | sb.WriteString(fmt.Sprintf("%s\n", m.search.View())) 93 | } 94 | return sb.String() 95 | } 96 | -------------------------------------------------------------------------------- /pkg/api/repository.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | "github.com/cli/go-gh/v2/pkg/api" 11 | "github.com/cli/go-gh/v2/pkg/config" 12 | gql "github.com/cli/shurcooL-graphql" 13 | ) 14 | 15 | type GitHubRepository struct { 16 | Name string `json:"name"` 17 | Url string `json:"url"` 18 | Description string `json:"description"` 19 | Readme string `json:"readme"` 20 | } 21 | 22 | type GitHubRepositories []GitHubRepository 23 | 24 | type RepositoryService interface { 25 | FindStarredRepositories(string) (FindStarredRepositoriesResult, error) 26 | } 27 | 28 | type FindStarredRepositoriesResult struct { 29 | Repositories []GitHubRepository 30 | PageInfo PageInfo 31 | } 32 | 33 | type repositoryService struct{} 34 | 35 | func NewRepositoryService() RepositoryService { 36 | return &repositoryService{} 37 | } 38 | 39 | // FindStarredRepositories は GitHub のスターをつけたリポジトリを指定されたカーソルから取得します 40 | func (s *repositoryService) FindStarredRepositories(after string) (FindStarredRepositoriesResult, error) { 41 | c, err := api.DefaultGraphQLClient() 42 | if err != nil { 43 | return FindStarredRepositoriesResult{}, err 44 | } 45 | 46 | q := getStarredRepositoriesQuery{} 47 | if err := q.Execute(c, map[string]interface{}{ 48 | "expression": gql.String("HEAD:README.md"), 49 | "after": gql.String(after), 50 | }); err != nil { 51 | return FindStarredRepositoriesResult{}, err 52 | } 53 | 54 | return FindStarredRepositoriesResult{ 55 | Repositories: q.Repositories(), 56 | PageInfo: q.PageInfo(), 57 | }, nil 58 | } 59 | 60 | func GetStarredRepositoriesFromCache() (GitHubRepositories, error) { 61 | path := fmt.Sprintf("%s/starred_repositories.json", config.CacheDir()) 62 | file, err := os.Open(path) 63 | if err != nil { 64 | return nil, err 65 | } 66 | defer file.Close() 67 | 68 | decoder := json.NewDecoder(file) 69 | var cacheData struct { 70 | ExpiresAt string `json:"expires_at"` 71 | CreatedAt string `json:"created_at"` 72 | Data GitHubRepositories `json:"data"` 73 | } 74 | 75 | err = decoder.Decode(&cacheData) 76 | if err != nil { 77 | return nil, err 78 | } 79 | expiresAt, err := time.Parse(time.RFC3339, cacheData.ExpiresAt) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | if time.Now().After(expiresAt) { 85 | return nil, errors.New("cache expired") 86 | } 87 | 88 | respositories := make(GitHubRepositories, len(cacheData.Data)) 89 | for i, d := range cacheData.Data { 90 | respositories[i] = d 91 | } 92 | 93 | return respositories, nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "os" 8 | "time" 9 | 10 | "github.com/rokuosan/gh-find-starred/pkg/api" 11 | ) 12 | 13 | type CacheController interface { 14 | Get(key string) (interface{}, error) 15 | Cache(key string, data interface{}) error 16 | } 17 | 18 | type PeriodicalCache struct { 19 | Period time.Duration 20 | Path string 21 | } 22 | 23 | type PeriodicalCacheData struct { 24 | ExpiresAt string `json:"expires_at"` 25 | CreatedAt string `json:"created_at"` 26 | Data []interface{} `json:"data"` 27 | } 28 | 29 | func NewPeriodicalCache(path string, period time.Duration) *PeriodicalCache { 30 | return &PeriodicalCache{ 31 | Period: period, 32 | Path: path, 33 | } 34 | } 35 | 36 | func (c *PeriodicalCache) Get(key string) (interface{}, error) { 37 | 38 | return nil, nil 39 | } 40 | 41 | func (c *PeriodicalCache) Cache(key string, data interface{}) error { 42 | now := time.Now().Format(time.RFC3339) 43 | expiresAt := time.Now().Add(c.Period).Format(time.RFC3339) 44 | cacheData := PeriodicalCacheData{ 45 | ExpiresAt: expiresAt, 46 | CreatedAt: now, 47 | Data: []interface{}{data}, 48 | } 49 | 50 | jsonData, err := c.toJSON(cacheData) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | file, err := os.Create(c.Path) 56 | if err != nil { 57 | return err 58 | } 59 | defer file.Close() 60 | 61 | return c.write(jsonData, file) 62 | } 63 | 64 | func (c *PeriodicalCache) toJSON(data PeriodicalCacheData) ([]byte, error) { 65 | buffer := bytes.NewBuffer([]byte{}) 66 | encoder := json.NewEncoder(buffer) 67 | err := encoder.Encode(data) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return buffer.Bytes(), nil 72 | } 73 | 74 | func (c *PeriodicalCache) write(data []byte, writer io.Writer) error { 75 | _, err := writer.Write(data) 76 | return err 77 | } 78 | 79 | type CacheData struct { 80 | ExpiresAt string `json:"expires_at"` 81 | CreatedAt string `json:"created_at"` 82 | Data []interface{} `json:"data"` 83 | } 84 | 85 | func Cache(path string, data api.GitHubRepositories) error { 86 | now := time.Now().Format(time.RFC3339) 87 | expiresAt := time.Now().Add(time.Hour * 24).Format(time.RFC3339) 88 | cacheData := CacheData{ 89 | ExpiresAt: expiresAt, 90 | CreatedAt: now, 91 | Data: make([]interface{}, len(data)), 92 | } 93 | for i, d := range data { 94 | cacheData.Data[i] = d 95 | } 96 | 97 | // ファイルを作成 98 | f, err := os.Create(path) 99 | if err != nil { 100 | return err 101 | } 102 | defer f.Close() 103 | 104 | // ファイルに書き込む 105 | enc := json.NewEncoder(f) 106 | if err := enc.Encode(cacheData); err != nil { 107 | return err 108 | } 109 | 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rokuosan/gh-find-starred 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/blevesearch/bleve/v2 v2.4.4 7 | github.com/charmbracelet/bubbles v0.20.0 8 | github.com/charmbracelet/bubbletea v1.2.4 9 | github.com/charmbracelet/lipgloss v1.0.0 10 | github.com/cli/go-gh/v2 v2.11.2 11 | github.com/cli/shurcooL-graphql v0.0.4 12 | github.com/spf13/cobra v1.8.1 13 | github.com/stretchr/testify v1.10.0 14 | ) 15 | 16 | require ( 17 | github.com/RoaringBitmap/roaring v1.9.3 // indirect 18 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 19 | github.com/bits-and-blooms/bitset v1.12.0 // indirect 20 | github.com/blevesearch/bleve_index_api v1.1.12 // indirect 21 | github.com/blevesearch/geo v0.1.20 // indirect 22 | github.com/blevesearch/go-faiss v1.0.24 // indirect 23 | github.com/blevesearch/go-porterstemmer v1.0.3 // indirect 24 | github.com/blevesearch/gtreap v0.1.1 // indirect 25 | github.com/blevesearch/mmap-go v1.0.4 // indirect 26 | github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect 27 | github.com/blevesearch/segment v0.9.1 // indirect 28 | github.com/blevesearch/snowballstem v0.9.0 // indirect 29 | github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect 30 | github.com/blevesearch/vellum v1.0.10 // indirect 31 | github.com/blevesearch/zapx/v11 v11.3.10 // indirect 32 | github.com/blevesearch/zapx/v12 v12.3.10 // indirect 33 | github.com/blevesearch/zapx/v13 v13.3.10 // indirect 34 | github.com/blevesearch/zapx/v14 v14.3.10 // indirect 35 | github.com/blevesearch/zapx/v15 v15.3.16 // indirect 36 | github.com/blevesearch/zapx/v16 v16.1.9-0.20241217210638-a0519e7caf3b // indirect 37 | github.com/charmbracelet/x/ansi v0.5.2 // indirect 38 | github.com/charmbracelet/x/term v0.2.1 // indirect 39 | github.com/cli/safeexec v1.0.0 // indirect 40 | github.com/davecgh/go-spew v1.1.1 // indirect 41 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 42 | github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect 43 | github.com/golang/protobuf v1.3.2 // indirect 44 | github.com/golang/snappy v0.0.1 // indirect 45 | github.com/henvic/httpretty v0.0.6 // indirect 46 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 47 | github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede // indirect 48 | github.com/kr/text v0.2.0 // indirect 49 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 50 | github.com/mattn/go-isatty v0.0.20 // indirect 51 | github.com/mattn/go-localereader v0.0.1 // indirect 52 | github.com/mattn/go-runewidth v0.0.16 // indirect 53 | github.com/mschoch/smat v0.2.0 // indirect 54 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 55 | github.com/muesli/cancelreader v0.2.2 // indirect 56 | github.com/muesli/termenv v0.15.2 // indirect 57 | github.com/pmezard/go-difflib v1.0.0 // indirect 58 | github.com/rivo/uniseg v0.4.7 // indirect 59 | github.com/spf13/pflag v1.0.5 // indirect 60 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect 61 | go.etcd.io/bbolt v1.3.7 // indirect 62 | golang.org/x/sync v0.10.0 // indirect 63 | golang.org/x/sys v0.28.0 // indirect 64 | golang.org/x/term v0.27.0 // indirect 65 | golang.org/x/text v0.21.0 // indirect 66 | gopkg.in/yaml.v3 v3.0.1 // indirect 67 | ) 68 | -------------------------------------------------------------------------------- /pkg/ui/fetch.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/bubbles/spinner" 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/lipgloss" 9 | "github.com/cli/go-gh/v2/pkg/config" 10 | "github.com/rokuosan/gh-find-starred/internal/cache" 11 | "github.com/rokuosan/gh-find-starred/pkg/api" 12 | ) 13 | 14 | type FetchStatus int 15 | 16 | const ( 17 | FetchStatusFailed FetchStatus = iota - 1 18 | FetchStatusLoading 19 | FetchStatusCompleted 20 | ) 21 | 22 | type FetchingModel struct { 23 | Spinner spinner.Model 24 | Status FetchStatus 25 | Repositories api.GitHubRepositories 26 | repositoryService api.RepositoryService 27 | err error 28 | cursor string 29 | fromCache bool 30 | } 31 | 32 | type FetchMsg struct { 33 | Repositories api.GitHubRepositories 34 | PageInfo api.PageInfo 35 | Err error 36 | fromCache bool 37 | } 38 | 39 | func NewDefaultFetchingModel() FetchingModel { 40 | s := spinner.New(spinner.WithSpinner(spinner.Dot)) 41 | s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) 42 | 43 | return FetchingModel{ 44 | Spinner: s, 45 | Repositories: api.GitHubRepositories{}, 46 | repositoryService: api.NewRepositoryService(), 47 | Status: FetchStatusLoading, 48 | } 49 | } 50 | 51 | func (m FetchingModel) Init() tea.Cmd { 52 | return tea.Batch( 53 | m.Spinner.Tick, 54 | m.GetRepositoriesFromGitHub, 55 | ) 56 | } 57 | 58 | func (m FetchingModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 59 | switch msg := msg.(type) { 60 | case spinner.TickMsg: 61 | if m.Status == FetchStatusLoading { 62 | var cmd tea.Cmd 63 | m.Spinner, cmd = m.Spinner.Update(msg) 64 | return m, cmd 65 | } 66 | return m, nil 67 | 68 | case FetchMsg: 69 | if msg.Err != nil { 70 | m.Status = FetchStatusFailed 71 | m.err = msg.Err 72 | return m, tea.Quit 73 | } 74 | m.Repositories = append(m.Repositories, msg.Repositories...) 75 | m.cursor = msg.PageInfo.EndCursor 76 | if msg.PageInfo.HasNextPage { 77 | return m, m.GetRepositoriesFromGitHub 78 | } 79 | // 全てのリポジトリを取得した場合は完了 80 | m.Status = FetchStatusCompleted 81 | // 取得したリポジトリをキャッシュする 82 | m.fromCache = msg.fromCache 83 | if !msg.fromCache { 84 | path := fmt.Sprintf("%s/starred_repositories.json", config.CacheDir()) 85 | if err := cache.Cache(path, m.Repositories); err != nil { 86 | m.Status = FetchStatusFailed 87 | m.err = err 88 | return m, tea.Quit 89 | } 90 | } 91 | 92 | return m, nil 93 | 94 | default: 95 | return m, nil 96 | } 97 | } 98 | 99 | func (m FetchingModel) View() string { 100 | switch m.Status { 101 | case FetchStatusFailed: 102 | return fmt.Sprintf("✗ Failed to fetch starred repositories:\nError:\n%v", m.err) 103 | case FetchStatusLoading: 104 | return fmt.Sprintf("%s Fetching starred repositories: %d", m.Spinner.View(), len(m.Repositories)) 105 | } 106 | 107 | msg := fmt.Sprintf("✓ Fetched starred repositories: %d", len(m.Repositories)) 108 | if m.fromCache { 109 | msg += " (from cache)" 110 | } 111 | return msg 112 | } 113 | 114 | func (m FetchingModel) GetRepositoriesFromGitHub() tea.Msg { 115 | // 初回実行のみキャッシュを取得してみる 116 | if len(m.Repositories) == 0 { 117 | if repos, err := api.GetStarredRepositoriesFromCache(); err == nil { 118 | return FetchMsg{ 119 | Repositories: repos, 120 | fromCache: true, 121 | } 122 | } 123 | } 124 | 125 | // 現在のカーソルからリポジトリを取得する 126 | result, err := m.repositoryService.FindStarredRepositories(m.cursor) 127 | if err != nil { 128 | return FetchMsg{Err: err} 129 | } 130 | // 取得したリポジトリを返す 131 | return FetchMsg{ 132 | Repositories: result.Repositories, 133 | PageInfo: result.PageInfo, 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= 2 | github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA= 6 | github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 7 | github.com/blevesearch/bleve/v2 v2.4.4 h1:RwwLGjUm54SwyyykbrZs4vc1qjzYic4ZnAnY9TwNl60= 8 | github.com/blevesearch/bleve/v2 v2.4.4/go.mod h1:fa2Eo6DP7JR+dMFpQe+WiZXINKSunh7WBtlDGbolKXk= 9 | github.com/blevesearch/bleve_index_api v1.1.12 h1:P4bw9/G/5rulOF7SJ9l4FsDoo7UFJ+5kexNy1RXfegY= 10 | github.com/blevesearch/bleve_index_api v1.1.12/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8= 11 | github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM= 12 | github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w= 13 | github.com/blevesearch/go-faiss v1.0.24 h1:K79IvKjoKHdi7FdiXEsAhxpMuns0x4fM0BO93bW5jLI= 14 | github.com/blevesearch/go-faiss v1.0.24/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= 15 | github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= 16 | github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= 17 | github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= 18 | github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= 19 | github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= 20 | github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= 21 | github.com/blevesearch/scorch_segment_api/v2 v2.2.16 h1:uGvKVvG7zvSxCwcm4/ehBa9cCEuZVE+/zvrSl57QUVY= 22 | github.com/blevesearch/scorch_segment_api/v2 v2.2.16/go.mod h1:VF5oHVbIFTu+znY1v30GjSpT5+9YFs9dV2hjvuh34F0= 23 | github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= 24 | github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= 25 | github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= 26 | github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= 27 | github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= 28 | github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= 29 | github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI= 30 | github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= 31 | github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk= 32 | github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ= 33 | github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s= 34 | github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs= 35 | github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8= 36 | github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk= 37 | github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU= 38 | github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns= 39 | github.com/blevesearch/zapx/v15 v15.3.16 h1:Ct3rv7FUJPfPk99TI/OofdC+Kpb4IdyfdMH48sb+FmE= 40 | github.com/blevesearch/zapx/v15 v15.3.16/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg= 41 | github.com/blevesearch/zapx/v16 v16.1.9-0.20241217210638-a0519e7caf3b h1:ju9Az5YgrzCeK3M1QwvZIpxYhChkXp7/L0RhDYsxXoE= 42 | github.com/blevesearch/zapx/v16 v16.1.9-0.20241217210638-a0519e7caf3b/go.mod h1:BlrYNpOu4BvVRslmIG+rLtKhmjIaRhIbG8sb9scGTwI= 43 | github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= 44 | github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= 45 | github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= 46 | github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= 47 | github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= 48 | github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= 49 | github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E= 50 | github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= 51 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 52 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 53 | github.com/cli/go-gh/v2 v2.11.2 h1:oad1+sESTPNTiTvh3I3t8UmxuovNDxhwLzeMHk45Q9w= 54 | github.com/cli/go-gh/v2 v2.11.2/go.mod h1:vVFhi3TfjseIW26ED9itAR8gQK0aVThTm8sYrsZ5QTI= 55 | github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= 56 | github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= 57 | github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= 58 | github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk= 59 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 60 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 61 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 62 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 63 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 64 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 65 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 66 | github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo= 67 | github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= 68 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 69 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 70 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 71 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 72 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 73 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 74 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 75 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 76 | github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= 77 | github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= 78 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 79 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 80 | github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede h1:YrgBGwxMRK0Vq0WSCWFaZUnTsrA/PZE/xs1QZh+/edg= 81 | github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 82 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 83 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 84 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 85 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 86 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 87 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 88 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 89 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 90 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 91 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 92 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 93 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 94 | github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= 95 | github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= 96 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 97 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 98 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 99 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 100 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 101 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 102 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 103 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 104 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 105 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 106 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 107 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 108 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 109 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 110 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 111 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 112 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 113 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 114 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 115 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 116 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= 117 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= 118 | go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= 119 | go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= 120 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 121 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 122 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 127 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 128 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 129 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 130 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 131 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 132 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 133 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 134 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 135 | gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= 136 | gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= 137 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 138 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 139 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 140 | --------------------------------------------------------------------------------