diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..96faaf1
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+ - package-ecosystem: gomod
+ directory: /
+ schedule:
+ interval: weekly
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml
new file mode 100644
index 0000000..67a73d9
--- /dev/null
+++ b/.github/workflows/automerge.yml
@@ -0,0 +1,17 @@
+name: Dependabot auto-merge
+
+on: pull_request
+
+permissions:
+ contents: write
+
+jobs:
+ dependabot:
+ runs-on: ubuntu-latest
+ if: ${{ github.actor == 'dependabot[bot]' }}
+ steps:
+ - name: Enable auto-merge for Dependabot PRs
+ run: gh pr merge --auto --merge "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..ae4c186
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,41 @@
+name: test
+on:
+ push:
+ pull_request:
+
+permissions:
+ contents: read
+ # Optional: allow read access to pull request. Use with `only-new-issues` option.
+ # pull-requests: read
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-go@v5
+ with:
+ go-version: "1.22"
+ cache: true
+
+ - name: Install dependencies
+ run: make init
+
+ - name: Lint
+ run: make lint-check
+
+ - name: check-is-dirty
+ run: |
+ if [[ -n $(git status --porcelain) ]]; then
+ echo "Detected uncommitted changes."
+ git status
+ git diff
+ exit 1
+ fi
+
+ - name: Run tests
+ run: make test
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..d2100fa
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,32 @@
+name: goreleaser
+
+on:
+ release:
+ types: [released, prereleased]
+
+permissions:
+ contents: write
+
+jobs:
+ goreleaser:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.22"
+ cache: true
+
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v5
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release --clean
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f9bee2f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+
+./fork-sweeper
+.DS_Store
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..a09114e
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,53 @@
+# This is an example .goreleaser.yml file with some sensible defaults.
+# Make sure to check the documentation at https://goreleaser.com
+builds:
+ - env:
+ - CGO_ENABLED=0
+ goos:
+ - linux
+ - windows
+ - darwin
+ goarch:
+ - amd64
+ - arm64
+ - "386"
+ main: ./cmd/fork-sweeper
+
+archives:
+ - format: tar.gz
+ # this name template makes the OS and Arch compatible with the results of uname.
+ name_template: >-
+ {{ .ProjectName }}_
+ {{- title .Os }}_
+ {{- if eq .Arch "amd64" }}x86_64
+ {{- else if eq .Arch "386" }}i386
+ {{- else }}{{ .Arch }}{{ end }}
+ {{- if .Arm }}v{{ .Arm }}_{{ .Version }}{{ end }}
+ # use zip for windows archives
+ format_overrides:
+ - goos: windows
+ format: zip
+
+checksum:
+ name_template: "checksums.txt"
+
+snapshot:
+ name_template: "{{ incpatch .Version }}-next"
+
+changelog:
+ sort: asc
+ filters:
+ exclude:
+ - "^docs:"
+ - "^test:"
+
+brews:
+ - name: link-patrol
+ homepage: "https://github.com/rednafi/fork-sweeper"
+ description: Remove unused GitHub forks
+ repository:
+ owner: rednafi
+ name: fork-sweeper
+ commit_author:
+ name: rednafi
+ email: redowan.nafi@gmail.com
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..a7fbe38
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,15 @@
+public/
+static/
+layouts/
+themes/
+
+# Ignore
+**/*.html
+**/Makefile
+.editorconfig
+.prettierignore
+.git*
+.hugo*
+Brew*
+pyproject*
+requirements*
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..de2042c
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,24 @@
+{
+ "proseWrap": "preserve",
+ "semi": false,
+ "singleQuote": false,
+ "useTabs": false,
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "printWidth": 92,
+ "overrides": [
+ {
+ "files": "**/*.md",
+ "options": {
+ "tabWidth": 4,
+ "useTabs": false,
+ "singleQuote": false,
+ "trailingComma": "all",
+ "arrowParens": "avoid",
+ "printWidth": 92,
+ "proseWrap": "always",
+ "embeddedLanguageFormatting": "off"
+ }
+ }
+ ]
+}
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..df1cbcf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,38 @@
+.PHONY: all build clean test lint ci
+
+# Binary name for the output binary
+BINARY_NAME=fork-sweeper
+
+# Default command to run when no arguments are provided to make
+all: build
+
+# Builds the binary
+build:
+ @echo "Building..."
+ go build -C cmd/fork-sweeper -o ../../${BINARY_NAME}
+
+# Cleans our project: deletes binaries
+clean:
+ @echo "Cleaning..."
+ go clean
+ rm -f ${BINARY_NAME}
+
+# Runs tests
+test:
+ @echo "Running tests..."
+ go test ./... -cover
+
+# Lints the project
+lint:
+ @echo "Linting..."
+ go fmt ./...
+ go vet ./...
+ go mod tidy
+
+# Command for Continuous Integration
+ci: lint test
+ @echo "CI steps..."
+ # Add commands specific to your CI setup
+ # e.g., integration testing, deployment commands, etc.
+
+# Additional commands can be added below for database migrations, Docker operations, etc.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b3c3794
--- /dev/null
+++ b/README.md
@@ -0,0 +1,81 @@
+
+
+
+fork
+sweeper
+|
+\|/
+-|-
+// \\
+/// \\\
+//// \\\\
+
+
+Remove unused GitHub forks
+
+
+
+
+## Installation
+
+- On macOS, brew install:
+
+ ```sh
+ brew install fork-sweeper
+ ```
+
+- Elsewhere, go install:
+
+ ```sh
+ go install fork-sweeper
+ ```
+
+## Prerequisites
+
+- Collect your GitHub API [access token]. The token will be sent as `Bearer ` in
+ the HTTP header while making API requests.
+- The token must have write and delete access to the forked repos.
+- Set the `GITHUB_TOKEN` to your current shell environment with
+ `export GITHUB_TOKEN=` command.
+
+## Usage
+
+- Run help:
+
+ ```sh
+ fork-sweeper -h
+ ```
+
+ ```txt
+ Usage of fork-sweeper:
+ -delete
+ Delete forked repos
+ -max-page int
+ Maximum page number to fetch (default 100)
+ -older-than-days int
+ Delete forked repos older than this number of days (default 60)
+ -owner string
+ GitHub repo owner (required)
+ -per-page int
+ Number of forked repos fetched per page (default 100)
+ -token string
+ GitHub access token (required)
+ -version
+ ```
+
+- List forked repos older than `n` days. By default it'll fetch all reposties that were
+ forked at least 60 days ago.
+
+ ```sh
+ fork-sweeper --owner rednafi --token $GITHUB_TOKEN --older-than-days 60
+ ```
+
+- The CLI won't delete any repository unless you explicitly tell it to do so with the
+ `--delete` flag:
+
+ ```sh
+ fork-sweeper --owner rednafi --token $GITHUB_TOKEN --delete
+ ```
+
+[access token]:
+ https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28
diff --git a/cmd/fork-pruner/main.go b/cmd/fork-pruner/main.go
deleted file mode 100644
index fc72808..0000000
--- a/cmd/fork-pruner/main.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package main
-
-import (
-
- "github.com/rednafi/fork-pruner/src"
-)
-
-func main() {
- src.CLI()
-}
diff --git a/cmd/fork-sweeper/main.go b/cmd/fork-sweeper/main.go
new file mode 100644
index 0000000..8ad6181
--- /dev/null
+++ b/cmd/fork-sweeper/main.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+ "github.com/rednafi/fork-sweeper/src"
+ "os"
+ "text/tabwriter"
+)
+
+// Ldflags filled by goreleaser
+var version = "dev"
+
+func main() {
+ w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
+ defer w.Flush()
+
+ cliConfig := src.NewCLIConfig(w, version, os.Exit)
+
+ cliConfig.CLI(os.Args[1:])
+}
diff --git a/go.mod b/go.mod
index 1559e91..ec8d57b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
-module github.com/rednafi/fork-pruner
+module github.com/rednafi/fork-sweeper
go 1.22.0
diff --git a/src/cli.go b/src/cli.go
index c072e1d..0502b01 100644
--- a/src/cli.go
+++ b/src/cli.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"flag"
"fmt"
+ "io"
"net/http"
"os"
"sync"
@@ -29,7 +30,11 @@ type repo struct {
CreatedAt time.Time `json:"created_at"`
}
-var httpClient = &http.Client{Timeout: 10 * time.Second}
+var httpClientPool = sync.Pool{
+ New: func() any {
+ return &http.Client{Timeout: 10 * time.Second}
+ },
+}
func fetchForkedReposPage(
ctx context.Context,
@@ -37,7 +42,8 @@ func fetchForkedReposPage(
owner,
token string,
pageNum,
- perPage int) ([]repo, error) {
+ perPage,
+ olderThanDays int) ([]repo, error) {
url := fmt.Sprintf(
"%s/users/%s/repos?type=forks&page=%d&per_page=%d",
@@ -59,8 +65,11 @@ func fetchForkedReposPage(
}
var forkedRepos []repo
+
+ cutOffDate := time.Now().AddDate(0, 0, -olderThanDays)
+
for _, repo := range repos {
- if repo.IsFork {
+ if repo.IsFork && repo.CreatedAt.Before(cutOffDate) {
forkedRepos = append(forkedRepos, repo)
}
}
@@ -74,11 +83,20 @@ func fetchForkedRepos(
owner,
token string,
perPage,
- maxPage int) ([]repo, error) {
+ maxPage,
+ olderThanDays int) ([]repo, error) {
var allRepos []repo
for pageNum := 1; pageNum <= maxPage; pageNum++ {
- repos, err := fetchForkedReposPage(ctx, baseURL, owner, token, pageNum, perPage)
+ repos, err := fetchForkedReposPage(
+ ctx,
+ baseURL,
+ owner,
+ token,
+ pageNum,
+ perPage,
+ olderThanDays)
+
if err != nil {
return nil, err
}
@@ -93,13 +111,16 @@ func fetchForkedRepos(
}
func doRequest(req *http.Request, v any) error {
+ httpClient := httpClientPool.Get().(*http.Client)
+ defer httpClientPool.Put(httpClient)
+
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
- if resp.StatusCode >= 400 {
+ if resp.StatusCode >= http.StatusBadRequest {
return fmt.Errorf("API request failed with status: %d", resp.StatusCode)
}
@@ -153,26 +174,123 @@ func printWithColor(color, text string) {
fmt.Println(color + text + Reset)
}
-func CLI() {
+type CLIConfig struct {
+
+ // Required
+ writer io.Writer
+ version string
+ exitFunc func(int)
+
+ // Optional
+ flagErrorHandling flag.ErrorHandling
+ printWithColor func(color, text string)
+ fetchForkedRepos func(
+ ctx context.Context,
+ baseURL,
+ owner,
+ token string,
+ perPage,
+ maxPage,
+ olderThanDays int) ([]repo, error)
+ deleteRepos func(ctx context.Context, baseURL, token string, repos []repo) error
+}
+
+// Dysfunctional options pattern
+
+func (c *CLIConfig) WithFlagErrorHandling(h flag.ErrorHandling) *CLIConfig {
+ c.flagErrorHandling = h
+ return c
+}
+
+func (c *CLIConfig) WithPrintWithColor(f func(color, text string)) *CLIConfig {
+ c.printWithColor = printWithColor
+ return c
+}
+
+func (c *CLIConfig) WithFetchForkedRepos(
+ f func(
+ ctx context.Context,
+ baseURL,
+ owner,
+ token string,
+ perPage,
+ maxPage,
+ olderThanDays int) ([]repo, error)) *CLIConfig {
+
+ c.fetchForkedRepos = f
+ return c
+}
+
+func (c *CLIConfig) WithDeleteRepos(
+ f func(ctx context.Context, baseURL, token string, repos []repo) error) *CLIConfig {
+
+ c.deleteRepos = f
+ return c
+}
+
+func NewCLIConfig(
+ writer io.Writer,
+ version string,
+ exitFunc func(int),
+) *CLIConfig {
+
+ return &CLIConfig{
+ writer: writer,
+ version: version,
+ exitFunc: exitFunc,
+ flagErrorHandling: flag.ExitOnError,
+ printWithColor: printWithColor,
+ fetchForkedRepos: fetchForkedRepos,
+ deleteRepos: deleteRepos,
+ }
+}
+
+func (c *CLIConfig) CLI(args []string) {
var (
- owner string
- token string
- perPage int
- maxPage int
+ owner string
+ token string
+ perPage int
+ maxPage int
+ olderThanDays int
+ version bool
+ delete bool
+ writer = c.writer
+ versionNum = c.version
+ exitFunc = c.exitFunc
+ flagErrorHandling = c.flagErrorHandling
+ printWithColor = c.printWithColor
+ fetchForkedRepos = c.fetchForkedRepos
+ deleteRepos = c.deleteRepos
)
// Parsing command-line flags
- flag.StringVar(&owner, "owner", "", "GitHub repository owner (required)")
- flag.StringVar(&token, "token", "", "GitHub personal access token (required)")
- flag.IntVar(&perPage, "per-page", 100, "Number of repositories per page")
- flag.IntVar(&maxPage, "max-page", 100, "Maximum page number to fetch")
- flag.Parse()
+ fs := flag.NewFlagSet("fork-sweeper", flagErrorHandling)
+ fs.SetOutput(writer)
+
+ fs.StringVar(&owner, "owner", "", "GitHub repo owner (required)")
+ fs.StringVar(&token, "token", "", "GitHub access token (required)")
+ fs.IntVar(&perPage, "per-page", 100, "Number of forked repos fetched per page")
+ fs.IntVar(&maxPage, "max-page", 100, "Maximum page number to fetch")
+ fs.IntVar(
+ &olderThanDays,
+ "older-than-days",
+ 60,
+ "Delete forked repos older than this number of days")
+ fs.BoolVar(&version, "version", false, "Print version")
+ fs.BoolVar(&delete, "delete", false, "Delete forked repos")
+ fs.Parse(args)
+
+ // Printing version
+ if version {
+ fmt.Println(versionNum)
+ return
+ }
// Validating required arguments
if owner == "" || token == "" {
fmt.Fprintf(os.Stderr, "%sError:%s Owner and token are required.\n", Red, Reset)
- flag.PrintDefaults()
- os.Exit(1)
+ fs.PrintDefaults()
+ exitFunc(1)
}
ctx := context.Background()
@@ -180,10 +298,18 @@ func CLI() {
// Fetching repositories
printWithColor(Blue, fmt.Sprintf("\nFetching repositories for %s...\n", owner))
- forkedRepos, err := fetchForkedRepos(ctx, baseURL, owner, token, perPage, maxPage)
+ forkedRepos, err := fetchForkedRepos(
+ ctx,
+ baseURL,
+ owner,
+ token,
+ perPage,
+ maxPage,
+ olderThanDays)
+
if err != nil {
fmt.Fprintf(os.Stderr, "%sError fetching repositories:%s %v\n", Red, Reset, err)
- os.Exit(1)
+ exitFunc(1)
}
if len(forkedRepos) == 0 {
@@ -198,10 +324,14 @@ func CLI() {
}
// Deleting forked repositories
+ if !delete {
+ return
+ }
+
printWithColor(Blue, "\nDeleting forked repositories...\n")
if err := deleteRepos(ctx, baseURL, token, forkedRepos); err != nil {
fmt.Fprintf(os.Stderr, "%sError deleting repositories:%s %v\n", Red, Reset, err)
- os.Exit(1)
+ exitFunc(1)
}
printWithColor(Green, "Deletion completed successfully.")
}
diff --git a/src/cli_test.go b/src/cli_test.go
index 2f69ad4..b4b44b5 100644
--- a/src/cli_test.go
+++ b/src/cli_test.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
+ "flag"
"fmt"
"io"
"net/http"
@@ -51,7 +52,9 @@ func TestUnmarshalRepo(t *testing.T) {
// Compare the expected and actual repo structs
if !reflect.DeepEqual(expected, result) {
- t.Errorf("Unmarshalled repo does not match expected value. Expected %+v, got %+v", expected, result)
+ t.Errorf(
+ `Unmarshalled repo does not match expected value.
+ Expected %+v, got %+v`, expected, result)
}
}
@@ -83,7 +86,7 @@ func TestFetchForkedReposPage(t *testing.T) {
}
forkedRepos, err := fetchForkedReposPage(
- context.Background(), mockServer.URL, "example", "fake-token", 1, 10)
+ context.Background(), mockServer.URL, "example", "fake-token", 1, 10, 60)
if err != nil {
t.Fatalf("fetchForkedReposPage returned an error: %v", err)
}
@@ -145,7 +148,13 @@ func TestFetchForkedRepos(t *testing.T) {
}
forkedRepos, err := fetchForkedRepos(
- context.Background(), mockServer.URL, "example", "fake-token", 10, 1)
+ context.Background(),
+ mockServer.URL,
+ "example",
+ "fake-token",
+ 10,
+ 1,
+ 60)
if err != nil {
t.Fatalf("fetchForkedRepos returned an error: %v", err)
}
@@ -323,3 +332,144 @@ func TestPrintWithColor(t *testing.T) {
})
}
}
+
+// Test cli flow
+
+// Mock functions to replace actual behavior in tests
+var (
+ mockExitFunc = func(called *int) func(int) {
+ return func(code int) {
+ *called++
+ }
+ }
+ mockFlagErrorHandler = flag.ContinueOnError
+ mockPrintWithColor = func(color, text string) { fmt.Println("mockPrintWithColor") }
+
+ mockFetchForkedRepos = func(
+ ctx context.Context,
+ baseURL,
+ owner,
+ token string,
+ perPage,
+ maxPage,
+ olderThanDays int) ([]repo, error) {
+ fmt.Println("mockFetchForkedRepos")
+ return []repo{{Name: "test-repo"}}, nil
+ }
+
+ mockDeleteRepos = func(
+ ctx context.Context,
+ baseURL,
+ token string,
+ repos []repo) error {
+ fmt.Println("mockDeleteRepos")
+ return nil
+ }
+)
+
+func TestNewCLIConfig_Defaults(t *testing.T) {
+ t.Parallel()
+ config := NewCLIConfig(nil, "", nil)
+
+ if config.printWithColor == nil || config.fetchForkedRepos == nil || config.deleteRepos == nil {
+ t.Fatal("Default functions were not set correctly")
+ }
+}
+
+func TestWithFlagErrorHandling_Option(t *testing.T) {
+ t.Parallel()
+ config := NewCLIConfig(nil, "", nil).WithFlagErrorHandling(mockFlagErrorHandler)
+ if config.flagErrorHandling != mockFlagErrorHandler {
+ t.Fatal("WithFlagErrorHandling did not set the flag error handling")
+ }
+}
+
+func TestWithPrintWithColor_Option(t *testing.T) {
+ t.Parallel()
+ config := NewCLIConfig(nil, "", nil).WithPrintWithColor(mockPrintWithColor)
+ if config.printWithColor == nil {
+ t.Fatal("WithPrintWithColor did not set the function")
+ }
+}
+
+func TestWithFetchForkedRepos_Option(t *testing.T) {
+ t.Parallel()
+ config := NewCLIConfig(nil, "", nil).WithFetchForkedRepos(mockFetchForkedRepos)
+
+ if config.fetchForkedRepos == nil {
+ t.Fatal("WithFetchForkedRepos did not set the function")
+ }
+}
+
+func TestWithDeleteRepos_Option(t *testing.T) {
+ t.Parallel()
+ config := NewCLIConfig(nil, "", nil).WithDeleteRepos(mockDeleteRepos)
+ if config.deleteRepos == nil {
+ t.Fatal("WithDeleteRepos did not set the function")
+ }
+}
+
+func captureStderr(testFunc func()) string {
+ r, w, _ := os.Pipe()
+ originalStderr := os.Stderr
+ os.Stderr = w
+
+ outputChan := make(chan string)
+ go func() {
+ var buf bytes.Buffer
+ io.Copy(&buf, r)
+ outputChan <- buf.String()
+ }()
+
+ testFunc()
+
+ w.Close()
+ os.Stderr = originalStderr
+ return <-outputChan
+}
+
+func TestCLI_MissingOwnerToken(t *testing.T) {
+ t.Parallel()
+ exitCalls := new(int)
+ stderrOutput := captureStderr(
+ func() {
+ cliConfig := NewCLIConfig(
+ os.Stderr,
+ "test-version",
+ mockExitFunc(exitCalls),
+ ).WithFetchForkedRepos(mockFetchForkedRepos).
+ WithDeleteRepos(mockDeleteRepos).
+ WithPrintWithColor(mockPrintWithColor).
+ WithFlagErrorHandling(mockFlagErrorHandler)
+
+ // Execute the CLI
+ cliConfig.CLI([]string{"cmd"})
+ })
+
+ // Verify
+ if !strings.Contains(stderrOutput, "Owner and token are required") {
+ t.Errorf("Expected error message not found in output")
+ }
+
+ if *exitCalls != 1 {
+ t.Errorf("Expected os.Exit to be called once, got %d", *exitCalls)
+ }
+}
+func TestCLI_Success(t *testing.T) {
+ t.Parallel()
+ exitCalls := new(int)
+ cliConfig := NewCLIConfig(
+ os.Stderr,
+ "test-version",
+ mockExitFunc(exitCalls),
+ ).WithDeleteRepos(mockDeleteRepos).
+ WithFetchForkedRepos(mockFetchForkedRepos).
+ WithPrintWithColor(mockPrintWithColor).
+ WithFlagErrorHandling(mockFlagErrorHandler)
+
+ // Execute the CLI
+ args := []string{"cmd", "--owner", "testOwner", "--token", "testToken"}
+
+ cliConfig.CLI(args)
+
+}