summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-07 23:25:10 +0300
committerPaul Buetow <paul@buetow.org>2025-07-07 23:25:10 +0300
commit4526c8a171dbe40762c116e5b8a404f20131d2b1 (patch)
treeea3d544cbad995dabb616f4b6136e6e24a097524 /internal/cli
parent64095a2c8d5a3a72c55d7bd0737c5542a5aeee09 (diff)
feat: add comprehensive showcase generation with metadata and images
- Add --showcase flag to generate project showcases using Claude - Extract repository metadata (languages, commits, LOC, dates, license) - Support image extraction from README files (local and remote) - Add caching with --force flag to regenerate - Add exclude_from_showcase config option - Add standalone showcase mode (--showcase without sync) - Sort projects by recent activity (avg age of last 100 commits) - Output in Gemini Gemtext template format (.gmi.tpl) - Fix backup location fetching when --backup flag not set 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/flags.go4
-rw-r--r--internal/cli/showcase_handler.go35
-rw-r--r--internal/cli/showcase_only_handler.go112
3 files changed, 151 insertions, 0 deletions
diff --git a/internal/cli/flags.go b/internal/cli/flags.go
index 322ad5a..7640398 100644
--- a/internal/cli/flags.go
+++ b/internal/cli/flags.go
@@ -25,6 +25,8 @@ type Flags struct {
Clean bool
DeleteRepo string
Backup bool
+ Showcase bool
+ Force bool
}
// ParseFlags parses command-line flags and returns the flags struct
@@ -50,6 +52,8 @@ func ParseFlags() *Flags {
flag.BoolVar(&f.Clean, "clean", false, "delete all repositories in work directory (with confirmation)")
flag.StringVar(&f.DeleteRepo, "delete-repo", "", "delete specified repository from all configured organizations (with confirmation)")
flag.BoolVar(&f.Backup, "backup", false, "enable syncing to backup locations")
+ flag.BoolVar(&f.Showcase, "showcase", false, "generate project showcase using Claude after syncing")
+ flag.BoolVar(&f.Force, "force", false, "force regeneration of cached data")
flag.Parse()
diff --git a/internal/cli/showcase_handler.go b/internal/cli/showcase_handler.go
new file mode 100644
index 0000000..a3dfd26
--- /dev/null
+++ b/internal/cli/showcase_handler.go
@@ -0,0 +1,35 @@
+package cli
+
+import (
+ "fmt"
+ "log"
+
+ "codeberg.org/snonux/gitsyncer/internal/config"
+ "codeberg.org/snonux/gitsyncer/internal/showcase"
+)
+
+// HandleShowcase handles the showcase generation after syncing
+func HandleShowcase(cfg *config.Config, flags *Flags) int {
+ // Determine which repositories to process
+ var repoFilter []string
+ if flags.SyncRepo != "" {
+ // Only process the specific repository that was synced
+ repoFilter = []string{flags.SyncRepo}
+ fmt.Printf("\nGenerating showcase for %s...\n", flags.SyncRepo)
+ } else {
+ // Process all repositories for --sync-all or public sync operations
+ fmt.Println("\nGenerating project showcase for all repositories...")
+ }
+
+ // Create showcase generator
+ generator := showcase.New(cfg, flags.WorkDir)
+
+ // Generate showcase with optional filter
+ if err := generator.GenerateShowcase(repoFilter, flags.Force); err != nil {
+ log.Printf("ERROR: Failed to generate showcase: %v\n", err)
+ return 1
+ }
+
+ fmt.Println("Showcase generated successfully!")
+ return 0
+} \ No newline at end of file
diff --git a/internal/cli/showcase_only_handler.go b/internal/cli/showcase_only_handler.go
new file mode 100644
index 0000000..ff2a82a
--- /dev/null
+++ b/internal/cli/showcase_only_handler.go
@@ -0,0 +1,112 @@
+package cli
+
+import (
+ "fmt"
+ "log"
+
+ "codeberg.org/snonux/gitsyncer/internal/codeberg"
+ "codeberg.org/snonux/gitsyncer/internal/config"
+ "codeberg.org/snonux/gitsyncer/internal/github"
+ "codeberg.org/snonux/gitsyncer/internal/showcase"
+ "codeberg.org/snonux/gitsyncer/internal/sync"
+)
+
+// HandleShowcaseOnly handles showcase generation without syncing
+// It will clone repositories if they don't exist locally, but won't sync changes
+func HandleShowcaseOnly(cfg *config.Config, flags *Flags) int {
+ // Get all repositories from all sources
+ allRepos, err := getAllRepositories(cfg)
+ if err != nil {
+ log.Printf("ERROR: Failed to get repositories: %v\n", err)
+ return 1
+ }
+
+ if len(allRepos) == 0 {
+ fmt.Println("No repositories found")
+ return 1
+ }
+
+ fmt.Printf("Found %d repositories total\n", len(allRepos))
+
+ // Create a minimal syncer just for cloning
+ syncer := sync.New(cfg, flags.WorkDir)
+ syncer.SetBackupEnabled(false) // Never use backup in showcase-only mode
+
+ // Ensure repositories are cloned (but not synced)
+ fmt.Println("\nEnsuring repositories are cloned locally...")
+ for _, repo := range allRepos {
+ if err := syncer.EnsureRepositoryCloned(repo); err != nil {
+ fmt.Printf("WARNING: Failed to clone %s: %v\n", repo, err)
+ // Continue with other repos
+ }
+ }
+
+ // Generate showcase for all repositories
+ fmt.Println("\nGenerating showcase for all repositories...")
+ generator := showcase.New(cfg, flags.WorkDir)
+
+ // Pass empty filter to process all repos
+ if err := generator.GenerateShowcase(nil, flags.Force); err != nil {
+ log.Printf("ERROR: Failed to generate showcase: %v\n", err)
+ return 1
+ }
+
+ fmt.Println("Showcase generation completed!")
+ return 0
+}
+
+// getAllRepositories collects all unique repository names from all sources
+func getAllRepositories(cfg *config.Config) ([]string, error) {
+ repoMap := make(map[string]bool)
+
+ // Add configured repositories
+ for _, repo := range cfg.Repositories {
+ repoMap[repo] = true
+ }
+
+ // Add Codeberg public repos if configured
+ if codebergOrg := cfg.FindCodebergOrg(); codebergOrg != nil {
+ fmt.Printf("Fetching public repositories from Codeberg user/org: %s...\n", codebergOrg.Name)
+ client := codeberg.NewClient(codebergOrg.Name, codebergOrg.CodebergToken)
+
+ repos, err := client.ListPublicRepos()
+ if err != nil {
+ // Try as user
+ repos, err = client.ListUserPublicRepos()
+ if err != nil {
+ fmt.Printf("Warning: Failed to fetch Codeberg repos: %v\n", err)
+ }
+ }
+
+ for _, repo := range repos {
+ repoMap[repo.Name] = true
+ }
+ }
+
+ // Add GitHub public repos if configured
+ if githubOrg := cfg.FindGitHubOrg(); githubOrg != nil {
+ fmt.Printf("Fetching public repositories from GitHub user/org: %s...\n", githubOrg.Name)
+ client := github.NewClient(githubOrg.GitHubToken, githubOrg.Name)
+
+ if client.HasToken() {
+ repos, err := client.ListPublicRepos()
+ if err != nil {
+ fmt.Printf("Warning: Failed to fetch GitHub repos: %v\n", err)
+ } else {
+ for _, repo := range repos {
+ repoMap[repo.Name] = true
+ }
+ }
+ } else {
+ fmt.Println("Warning: No GitHub token found, skipping GitHub repos")
+ }
+ }
+
+ // Convert map to slice
+ var allRepos []string
+ for repo := range repoMap {
+ allRepos = append(allRepos, repo)
+ }
+
+ return allRepos, nil
+} \ No newline at end of file